11"""Below code is generic boilerplate and normally should not be changed.
22
3- To avoid setup script boilerplate, create "setup.py" file with the following minimal contents
4- and modify them according to the specifics of your package.
3+ To avoid setup script boilerplate, create "setup.py" file with the minimal contents as given
4+ in SETUP_TEMPLATE below, and modify it according to the specifics of your package.
55
66See the implementation of setup_boilerplate.Package for default metadata values and available
77options.
88"""
99
10- import importlib
1110import pathlib
11+ import runpy
1212import sys
1313import typing as t
1414
15- import docutils .nodes
16- import docutils .parsers .rst
17- import docutils .utils
1815import setuptools
1916
20- __updated__ = '2018-04-18 '
17+ __updated__ = '2019-06-04 '
2118
2219SETUP_TEMPLATE = '''"""Setup script."""
2320
@@ -30,12 +27,13 @@ class Package(setup_boilerplate.Package):
3027
3128 name = ''
3229 description = ''
33- download_url = 'https://github.com/mbdevpl/...'
30+ url = 'https://github.com/mbdevpl/...'
3431 classifiers = [
3532 'Development Status :: 1 - Planning',
36- 'Programming Language :: Python :: 3.4',
3733 'Programming Language :: Python :: 3.5',
3834 'Programming Language :: Python :: 3.6',
35+ 'Programming Language :: Python :: 3.7',
36+ 'Programming Language :: Python :: 3.8',
3937 'Programming Language :: Python :: 3 :: Only']
4038 keywords = []
4139
@@ -50,10 +48,14 @@ class Package(setup_boilerplate.Package):
5048def find_version (
5149 package_name : str , version_module_name : str = '_version' ,
5250 version_variable_name : str = 'VERSION' ) -> str :
53- """Simulate behaviour of "from package_name._version import VERSION", and return VERSION."""
54- version_module = importlib .import_module (
55- '{}.{}' .format (package_name .replace ('-' , '_' ), version_module_name ))
56- return getattr (version_module , version_variable_name )
51+ """Simulate behaviour of "from package_name._version import VERSION", and return VERSION.
52+
53+ To avoid importing whole package only to read the version, just module containing the version
54+ is imported. Therefore relative imports in that module will break the setup.
55+ """
56+ version_module_path = '{}/{}.py' .format (package_name .replace ('-' , '_' ), version_module_name )
57+ version_module_vars = runpy .run_path (version_module_path )
58+ return version_module_vars [version_variable_name ]
5759
5860
5961def find_packages (root_directory : str = '.' ) -> t .List [str ]:
@@ -126,57 +128,59 @@ def find_required_python_version(
126128 return None
127129
128130
129- def parse_rst (text : str ) -> docutils .nodes .document :
130- """Parse text assuming it's an RST markup."""
131- parser = docutils .parsers .rst .Parser ()
132- components = (docutils .parsers .rst .Parser ,)
133- settings = docutils .frontend .OptionParser (components = components ).get_default_values ()
134- document = docutils .utils .new_document ('<rst-doc>' , settings = settings )
135- parser .parse (text , document )
136- return document
137-
138-
139- class SimpleRefCounter (docutils .nodes .NodeVisitor ):
140-
141- """Find all simple references in a given docutils document."""
142-
143- def __init__ (self , * args , ** kwargs ):
144- super ().__init__ (* args , ** kwargs )
145- self .references = []
146-
147- def visit_reference (self , node : docutils .nodes .reference ) -> None :
148- """Called for "reference" nodes."""
149- if len (node .children ) != 1 or not isinstance (node .children [0 ], docutils .nodes .Text ) \
150- or not all (_ in node .attributes for _ in ('name' , 'refuri' )):
151- return
152- path = pathlib .Path (node .attributes ['refuri' ])
153- try :
154- if path .is_absolute ():
155- return
156- resolved_path = path .resolve ()
157- except FileNotFoundError : # in resolve(), prior to Python 3.6
158- return
159- except OSError : # in is_absolute() and resolve(), on URLs in Windows
160- return
161- try :
162- resolved_path .relative_to (HERE )
163- except ValueError :
164- return
165- if not path .is_file ():
166- return
167- assert node .attributes ['name' ] == node .children [0 ].astext ()
168- self .references .append (node )
169-
170- def unknown_visit (self , node : docutils .nodes .Node ) -> None :
171- """Called for unknown node types."""
172- pass
173-
174-
175131def resolve_relative_rst_links (text : str , base_link : str ):
176132 """Resolve all relative links in a given RST document.
177133
178134 All links of form `link`_ become `link <base_link/link>`_.
179135 """
136+ import docutils .nodes
137+ import docutils .parsers .rst
138+ import docutils .utils
139+
140+ def parse_rst (text : str ) -> docutils .nodes .document :
141+ """Parse text assuming it's an RST markup."""
142+ parser = docutils .parsers .rst .Parser ()
143+ components = (docutils .parsers .rst .Parser ,)
144+ settings = docutils .frontend .OptionParser (components = components ).get_default_values ()
145+ document = docutils .utils .new_document ('<rst-doc>' , settings = settings )
146+ parser .parse (text , document )
147+ return document
148+
149+ class SimpleRefCounter (docutils .nodes .NodeVisitor ):
150+ """Find all simple references in a given docutils document."""
151+
152+ def __init__ (self , * args , ** kwargs ):
153+ """Initialize the SimpleRefCounter object."""
154+ super ().__init__ (* args , ** kwargs )
155+ self .references = []
156+
157+ def visit_reference (self , node : docutils .nodes .reference ) -> None :
158+ """Call for "reference" nodes."""
159+ if len (node .children ) != 1 or not isinstance (node .children [0 ], docutils .nodes .Text ) \
160+ or not all (_ in node .attributes for _ in ('name' , 'refuri' )):
161+ return
162+ path = pathlib .Path (node .attributes ['refuri' ])
163+ try :
164+ if path .is_absolute ():
165+ return
166+ resolved_path = path .resolve ()
167+ except FileNotFoundError : # in resolve(), prior to Python 3.6
168+ return
169+ except OSError : # in is_absolute() and resolve(), on URLs in Windows
170+ return
171+ try :
172+ resolved_path .relative_to (HERE )
173+ except ValueError :
174+ return
175+ if not path .is_file ():
176+ return
177+ assert node .attributes ['name' ] == node .children [0 ].astext ()
178+ self .references .append (node )
179+
180+ def unknown_visit (self , node : docutils .nodes .Node ) -> None :
181+ """Call for unknown node types."""
182+ return
183+
180184 document = parse_rst (text )
181185 visitor = SimpleRefCounter (document )
182186 document .walk (visitor )
@@ -192,7 +196,6 @@ def resolve_relative_rst_links(text: str, base_link: str):
192196
193197
194198class Package :
195-
196199 """Default metadata and behaviour for a Python package setup script."""
197200
198201 root_directory = '.' # type: str
@@ -208,16 +211,22 @@ class Package:
208211 long_description = None # type: str
209212 """If None, it will be generated from readme."""
210213
211- url = 'https://mbdevpl.github.io/' # type: str
212- download_url = 'https://github.com/mbdevpl' # type: str
214+ long_description_content_type = None # type: str
215+ """If None, it will be set accodring to readme file extension.
216+
217+ For this field to be automatically set, also long_description field has to be None.
218+ """
219+
220+ url = 'https://github.com/mbdevpl' # type: str
221+ download_url = None # type: str
213222 author = 'Mateusz Bysiek' # type: str
214- author_email = 'mb@mbdev.pl ' # type: str
223+ author_email = 'mateusz.bysiek@gmail.com ' # type: str
215224 # maintainer = None # type: str
216225 # maintainer_email = None # type: str
217226 license_str = 'Apache License 2.0' # type: str
218227
219228 classifiers = [] # type: t.List[str]
220- """List of valid project classifiers: https://pypi.python. org/pypi?:action=list_classifiers"""
229+ """List of valid project classifiers: https://pypi.org/pypi?:action=list_classifiers"""
221230
222231 keywords = [] # type: t.List[str]
223232
@@ -254,27 +263,33 @@ def try_fields(cls, *names) -> t.Optional[t.Any]:
254263 raise AttributeError ((cls , names ))
255264
256265 @classmethod
257- def parse_readme (cls , readme_path : str = 'README.rst' , encoding : str = 'utf-8' ) -> str :
266+ def parse_readme (cls , readme_path : str = 'README.rst' ,
267+ encoding : str = 'utf-8' ) -> t .Tuple [str , str ]:
258268 """Parse readme and resolve relative links in it if it is feasible.
259269
260270 Links are resolved if readme is in rst format and the package is hosted on GitHub.
261271 """
272+ readme_path = pathlib .Path (readme_path )
262273 with HERE .joinpath (readme_path ).open (encoding = encoding ) as readme_file :
263274 long_description = readme_file .read () # type: str
264275
265- if readme_path .endswith ( '.rst' ) and cls .download_url .startswith ('https://github.com/' ):
266- base_url = '{}/blob/v{}/' .format (cls .download_url , cls .version )
276+ if readme_path .suffix . lower () == '.rst' and cls .url .startswith ('https://github.com/' ):
277+ base_url = '{}/blob/v{}/' .format (cls .url , cls .version )
267278 long_description = resolve_relative_rst_links (long_description , base_url )
268279
269- return long_description
280+ long_description_content_type = {'.rst' : 'text/x-rst' , '.md' : 'text/markdown' }.get (
281+ readme_path .suffix .lower (), 'text/plain' )
282+ long_description_content_type += '; charset=UTF-8'
283+
284+ return long_description , long_description_content_type
270285
271286 @classmethod
272287 def prepare (cls ) -> None :
273288 """Fill in possibly missing package metadata."""
274289 if cls .version is None :
275290 cls .version = find_version (cls .name )
276291 if cls .long_description is None :
277- cls .long_description = cls .parse_readme ()
292+ cls .long_description , cls . long_description_content_type = cls .parse_readme ()
278293 if cls .packages is None :
279294 cls .packages = find_packages (cls .root_directory )
280295 if cls .install_requires is None :
@@ -284,11 +299,13 @@ def prepare(cls) -> None:
284299
285300 @classmethod
286301 def setup (cls ) -> None :
287- """Run setuptools.setup() with correct arguments."""
302+ """Call setuptools.setup with correct arguments."""
288303 cls .prepare ()
289304 setuptools .setup (
290305 name = cls .name , version = cls .version , description = cls .description ,
291- long_description = cls .long_description , url = cls .url , download_url = cls .download_url ,
306+ long_description = cls .long_description ,
307+ long_description_content_type = cls .long_description_content_type ,
308+ url = cls .url , download_url = cls .download_url ,
292309 author = cls .author , author_email = cls .author_email ,
293310 maintainer = cls .try_fields ('maintainer' , 'author' ),
294311 maintainer_email = cls .try_fields ('maintainer_email' , 'author_email' ),
0 commit comments