Skip to content

Commit 5e5ef79

Browse files
committed
update setup boilerplate
1 parent 4454182 commit 5e5ef79

File tree

3 files changed

+99
-100
lines changed

3 files changed

+99
-100
lines changed

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class Package(setup_boilerplate.Package):
99

1010
name = 'open-fortran-parser'
1111
description = 'Python wrapper for XML output generator for Open Fortran Parser'
12-
download_url = 'https://github.com/mbdevpl/open-fortran-parser-xml'
12+
url = 'https://github.com/mbdevpl/open-fortran-parser-xml'
1313
classifiers = [
1414
'Development Status :: 3 - Alpha',
1515
'Environment :: Console',

setup_boilerplate.py

Lines changed: 88 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,20 @@
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
66
See the implementation of setup_boilerplate.Package for default metadata values and available
77
options.
88
"""
99

10-
import importlib
1110
import pathlib
11+
import runpy
1212
import sys
1313
import typing as t
1414

15-
import docutils.nodes
16-
import docutils.parsers.rst
17-
import docutils.utils
1815
import setuptools
1916

20-
__updated__ = '2018-04-18'
17+
__updated__ = '2019-06-04'
2118

2219
SETUP_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):
5048
def 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

5961
def 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-
175131
def 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

194198
class 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'),

test/test_setup.py

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@
88
import subprocess
99
import sys
1010
import tempfile
11+
import types
1112
import typing as t
1213
import unittest
1314

14-
__updated__ = '2018-02-14'
15+
__updated__ = '2019-06-04'
1516

1617

1718
def run_program(*args, glob: bool = False):
@@ -41,7 +42,7 @@ def run_module(name: str, *args, run_name: str = '__main__') -> None:
4142
sys.argv = backup_sys_argv
4243

4344

44-
def import_module(name: str = 'setup') -> 'module':
45+
def import_module(name: str = 'setup') -> types.ModuleType:
4546
setup_module = importlib.import_module(name)
4647
return setup_module
4748

@@ -51,11 +52,6 @@ def import_module_member(module_name: str, member_name: str) -> t.Any:
5152
return getattr(module, member_name)
5253

5354

54-
# def import_module_members(module_name: str, member_names: t.Iterable[str]) -> t.List[t.Any]:
55-
# module = import_module(module_name)
56-
# return [getattr(module, member_name) for member_name in member_names]
57-
58-
5955
CLASSIFIERS_LICENSES = (
6056
'License :: OSI Approved :: Python License (CNRI Python License)',
6157
'License :: OSI Approved :: Python Software Foundation License',
@@ -64,28 +60,17 @@ def import_module_member(module_name: str, member_name: str) -> t.Any:
6460

6561
CLASSIFIERS_PYTHON_VERSIONS = tuple("""Programming Language :: Python
6662
Programming Language :: Python :: 2
67-
Programming Language :: Python :: 2.3
68-
Programming Language :: Python :: 2.4
69-
Programming Language :: Python :: 2.5
70-
Programming Language :: Python :: 2.6
63+
Programming Language :: Python :: 2.2
7164
Programming Language :: Python :: 2.7
7265
Programming Language :: Python :: 2 :: Only
7366
Programming Language :: Python :: 3
7467
Programming Language :: Python :: 3.0
75-
Programming Language :: Python :: 3.1
76-
Programming Language :: Python :: 3.2
77-
Programming Language :: Python :: 3.3
78-
Programming Language :: Python :: 3.4
7968
Programming Language :: Python :: 3.5
80-
Programming Language :: Python :: 3.6
81-
Programming Language :: Python :: 3.7
8269
Programming Language :: Python :: 3 :: Only""".splitlines())
8370

8471
CLASSIFIERS_PYTHON_IMPLEMENTATIONS = tuple("""Programming Language :: Python :: Implementation
8572
Programming Language :: Python :: Implementation :: CPython
86-
Programming Language :: Python :: Implementation :: IronPython
8773
Programming Language :: Python :: Implementation :: Jython
88-
Programming Language :: Python :: Implementation :: MicroPython
8974
Programming Language :: Python :: Implementation :: PyPy
9075
Programming Language :: Python :: Implementation :: Stackless""".splitlines())
9176

@@ -105,11 +90,6 @@ def import_module_member(module_name: str, member_name: str) -> t.Any:
10590

10691
CLASSIFIERS_PYTHON_IMPLEMENTATIONS_TUPLES = tuple((_,) for _ in CLASSIFIERS_PYTHON_IMPLEMENTATIONS)
10792

108-
# CLASSIFIERS_VARIOUS_PERMUTATIONS = tuple(itertools.chain.from_iterable(
109-
# itertools.permutations(..., n)
110-
# for n in range(...)
111-
# ))
112-
11393
CLASSIFIERS_VARIOUS_COMBINATIONS = tuple(itertools.combinations(
11494
CLASSIFIERS_VARIOUS, len(CLASSIFIERS_VARIOUS) - 1)) + (CLASSIFIERS_VARIOUS,)
11595

@@ -261,23 +241,25 @@ class Package(package): # pylint: disable=too-few-public-methods
261241
name = 'package name'
262242
description = 'package description'
263243
version = '1.2.3.4'
264-
download_url = 'https://github.com/example'
244+
url = 'https://github.com/example'
265245

266246
with tempfile.NamedTemporaryFile('w', suffix='.md', delete=False) as temp_file:
267247
temp_file.write('test test test')
268-
result = Package.parse_readme(temp_file.name)
248+
result, content_type = Package.parse_readme(temp_file.name)
269249
os.remove(temp_file.name)
270250
self.assertIsInstance(result, str)
251+
self.assertIsInstance(content_type, str)
271252

272253
prefix = 'https://github.com/example/blob/v1.2.3.4/'
273254
for name, link, done in LINK_EXAMPLES:
274255
name = '' if name is None else name + ' '
275256
text = 'Please see `{}<{}>`_ for details.'.format(name, link)
276257
with tempfile.NamedTemporaryFile('w', suffix='.rst', delete=False) as temp_file:
277258
temp_file.write(text)
278-
result = Package.parse_readme(temp_file.name)
259+
result, content_type = Package.parse_readme(temp_file.name)
279260
os.remove(temp_file.name)
280261
self.assertIsInstance(result, str)
262+
self.assertIsInstance(content_type, str)
281263
if not done:
282264
self.assertEqual(result, text)
283265
continue
@@ -313,7 +295,7 @@ class Package(package): # pylint: disable=too-few-public-methods, missing-docst
313295
Package.prepare()
314296

315297
Package.version = None
316-
with self.assertRaises(ImportError):
298+
with self.assertRaises(FileNotFoundError):
317299
Package.prepare()
318300

319301

0 commit comments

Comments
 (0)