version-query 1.4.0rc1__tar.gz → 1.5.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {version-query-1.4.0rc1 → version-query-1.5.2}/MANIFEST.in +0 -1
  2. {version-query-1.4.0rc1/version_query.egg-info → version-query-1.5.2}/PKG-INFO +7 -6
  3. {version-query-1.4.0rc1 → version-query-1.5.2}/README.rst +1 -1
  4. {version-query-1.4.0rc1 → version-query-1.5.2}/pyproject.toml +12 -1
  5. {version-query-1.4.0rc1 → version-query-1.5.2}/requirements.txt +0 -1
  6. version-query-1.5.2/requirements_test.txt +4 -0
  7. {version-query-1.4.0rc1 → version-query-1.5.2}/setup.py +3 -2
  8. version-query-1.5.2/test/test_packaging.py +7 -0
  9. {version-query-1.4.0rc1 → version-query-1.5.2}/test/test_query.py +3 -2
  10. {version-query-1.4.0rc1 → version-query-1.5.2}/version_query/git_query.py +6 -4
  11. {version-query-1.4.0rc1 → version-query-1.5.2/version_query.egg-info}/PKG-INFO +7 -6
  12. {version-query-1.4.0rc1 → version-query-1.5.2}/version_query.egg-info/SOURCES.txt +7 -2
  13. {version-query-1.4.0rc1 → version-query-1.5.2}/version_query.egg-info/requires.txt +0 -1
  14. {version-query-1.4.0rc1 → version-query-1.5.2}/version_query.egg-info/top_level.txt +1 -0
  15. version-query-1.4.0rc1/requirements_test.txt +0 -6
  16. version-query-1.4.0rc1/setup_boilerplate.py +0 -393
  17. version-query-1.4.0rc1/test/test_setup.py +0 -364
  18. {version-query-1.4.0rc1 → version-query-1.5.2}/LICENSE +0 -0
  19. {version-query-1.4.0rc1 → version-query-1.5.2}/NOTICE +0 -0
  20. {version-query-1.4.0rc1 → version-query-1.5.2}/setup.cfg +0 -0
  21. {version-query-1.4.0rc1 → version-query-1.5.2}/test/__init__.py +0 -0
  22. {version-query-1.4.0rc1 → version-query-1.5.2}/test/examples.py +0 -0
  23. {version-query-1.4.0rc1 → version-query-1.5.2}/test/test_git.py +0 -0
  24. {version-query-1.4.0rc1 → version-query-1.5.2}/test/test_version.py +0 -0
  25. {version-query-1.4.0rc1 → version-query-1.5.2}/test/test_with_git_repo.py +0 -0
  26. {version-query-1.4.0rc1 → version-query-1.5.2}/version_query/__init__.py +0 -0
  27. {version-query-1.4.0rc1 → version-query-1.5.2}/version_query/__main__.py +0 -0
  28. {version-query-1.4.0rc1 → version-query-1.5.2}/version_query/_version.py +0 -0
  29. {version-query-1.4.0rc1 → version-query-1.5.2}/version_query/main.py +0 -0
  30. {version-query-1.4.0rc1 → version-query-1.5.2}/version_query/py.typed +0 -0
  31. {version-query-1.4.0rc1 → version-query-1.5.2}/version_query/py_query.py +0 -0
  32. {version-query-1.4.0rc1 → version-query-1.5.2}/version_query/query.py +0 -0
  33. {version-query-1.4.0rc1 → version-query-1.5.2}/version_query/version.py +0 -0
  34. {version-query-1.4.0rc1 → version-query-1.5.2}/version_query.egg-info/dependency_links.txt +0 -0
@@ -1,4 +1,3 @@
1
- include setup_boilerplate.py
2
1
  include requirements.txt
3
2
  include LICENSE
4
3
  include NOTICE
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: version-query
3
- Version: 1.4.0rc1
3
+ Version: 1.5.2
4
4
  Summary: Zero-overhead package versioning for Python.
5
5
  Home-page: https://github.com/mbdevpl/version-query
6
6
  Download-URL:
@@ -18,6 +18,7 @@ Classifier: Natural Language :: English
18
18
  Classifier: Operating System :: MacOS :: MacOS X
19
19
  Classifier: Operating System :: Microsoft :: Windows
20
20
  Classifier: Operating System :: POSIX :: Linux
21
+ Classifier: Programming Language :: Python :: 3.8
21
22
  Classifier: Programming Language :: Python :: 3.9
22
23
  Classifier: Programming Language :: Python :: 3.10
23
24
  Classifier: Programming Language :: Python :: 3.11
@@ -27,7 +28,7 @@ Classifier: Topic :: Software Development :: Version Control :: Git
27
28
  Classifier: Topic :: System :: Software Distribution
28
29
  Classifier: Topic :: Utilities
29
30
  Classifier: Typing :: Typed
30
- Requires-Python: >=3.9
31
+ Requires-Python: >=3.8
31
32
  Description-Content-Type: text/x-rst; charset=UTF-8
32
33
  License-File: LICENSE
33
34
  License-File: NOTICE
@@ -62,7 +63,7 @@ Zero-overhead package versioning for Python.
62
63
  :alt: grade from Codacy
63
64
 
64
65
  .. image:: https://img.shields.io/github/license/mbdevpl/version-query.svg
65
- :target: https://github.com/mbdevpl/version-query/blob/v1.4.0rc1/NOTICE
66
+ :target: https://github.com/mbdevpl/version-query/blob/v1.5.2/NOTICE
66
67
  :alt: license
67
68
 
68
69
  Package versioning toolkit for Python which relies on git tags, git history traversal
@@ -470,10 +471,10 @@ using version-query without any issues.
470
471
  Requirements
471
472
  ============
472
473
 
473
- Python version 3.9 or later.
474
+ Python version 3.8 or later.
474
475
 
475
- Python libraries as specified in `requirements.txt <https://github.com/mbdevpl/version-query/blob/v1.4.0rc1/requirements.txt>`_.
476
+ Python libraries as specified in `requirements.txt <https://github.com/mbdevpl/version-query/blob/v1.5.2/requirements.txt>`_.
476
477
 
477
- Building and running tests additionally requires packages listed in `requirements_test.txt <https://github.com/mbdevpl/version-query/blob/v1.4.0rc1/requirements_test.txt>`_.
478
+ Building and running tests additionally requires packages listed in `requirements_test.txt <https://github.com/mbdevpl/version-query/blob/v1.5.2/requirements_test.txt>`_.
478
479
 
479
480
  Tested on Linux, OS X and Windows.
@@ -436,7 +436,7 @@ using version-query without any issues.
436
436
  Requirements
437
437
  ============
438
438
 
439
- Python version 3.9 or later.
439
+ Python version 3.8 or later.
440
440
 
441
441
  Python libraries as specified in `<requirements.txt>`_.
442
442
 
@@ -1,10 +1,21 @@
1
1
  [build-system]
2
- requires = ['docutils', 'GitPython', 'packaging', 'semver', 'setuptools', 'wheel']
2
+ requires = [
3
+ 'boilerplates[setup] ~= 0.1',
4
+ 'GitPython ~= 3.1',
5
+ 'packaging >= 23.0',
6
+ 'semver ~= 2.13'
7
+ ]
3
8
 
4
9
  [tool.flake8]
5
10
  max-line-length = 100
6
11
  max-doc-length = 100
7
12
 
13
+ [[tool.mypy.overrides]]
14
+ module = [
15
+ 'semver'
16
+ ]
17
+ ignore_missing_imports = true
18
+
8
19
  [tool.pydocstyle]
9
20
  ignore = [
10
21
  'D102', 'D103', 'D105', 'D107',
@@ -1,4 +1,3 @@
1
1
  GitPython ~= 3.1
2
2
  packaging >= 23.0
3
3
  semver ~= 2.13
4
- setuptools >= 67.4
@@ -0,0 +1,4 @@
1
+ -r requirements.txt
2
+ boilerplates[packaging_tests] ~= 0.1
3
+ colorlog ~= 6.7
4
+ setuptools >= 67.4
@@ -1,9 +1,9 @@
1
1
  """Setup script for version_query package."""
2
2
 
3
- import setup_boilerplate
3
+ import boilerplates.setup
4
4
 
5
5
 
6
- class Package(setup_boilerplate.Package):
6
+ class Package(boilerplates.setup.Package):
7
7
  """Package metadata."""
8
8
 
9
9
  name = 'version-query'
@@ -20,6 +20,7 @@ class Package(setup_boilerplate.Package):
20
20
  'Operating System :: MacOS :: MacOS X',
21
21
  'Operating System :: Microsoft :: Windows',
22
22
  'Operating System :: POSIX :: Linux',
23
+ 'Programming Language :: Python :: 3.8',
23
24
  'Programming Language :: Python :: 3.9',
24
25
  'Programming Language :: Python :: 3.10',
25
26
  'Programming Language :: Python :: 3.11',
@@ -0,0 +1,7 @@
1
+ """Tests for packaging."""
2
+
3
+ import boilerplates.packaging_tests
4
+
5
+
6
+ class Tests(boilerplates.packaging_tests.PackagingTests):
7
+ pass
@@ -10,6 +10,8 @@ import sys
10
10
  import tempfile
11
11
  import unittest
12
12
 
13
+ from boilerplates.packaging_tests import run_module
14
+
13
15
  from version_query.version import VersionComponent, Version
14
16
  from version_query.git_query import query_git_repo, predict_git_repo
15
17
  from version_query.py_query import query_metadata_json, query_pkg_info, query_package_folder
@@ -18,7 +20,6 @@ from version_query.query import \
18
20
  from .examples import \
19
21
  PY_LIB_DIR, GIT_REPO_EXAMPLES, METADATA_JSON_EXAMPLE_PATHS, PKG_INFO_EXAMPLE_PATHS, \
20
22
  PACKAGE_FOLDER_EXAMPLES
21
- from .test_setup import run_module
22
23
 
23
24
  _LOG = logging.getLogger(__name__)
24
25
 
@@ -61,7 +62,7 @@ class Tests(unittest.TestCase):
61
62
 
62
63
  def _query_test_case(self, paths, query_function):
63
64
  for path in paths:
64
- if any(_ in path.parts for _ in IGNORED_FOLDER_NAMES):
65
+ if any(_ in path.parts for _ in IGNORED_FOLDER_NAMES) or not path.exists():
65
66
  continue
66
67
  with self.subTest(path=path, query_function=query_function):
67
68
  _LOG.debug('testing %s() on %s', query_function.__name__, path)
@@ -46,7 +46,8 @@ def _latest_git_version_tag_on_branches(
46
46
  t.Optional[git.objects.Commit], t.Optional[git.TagReference], t.Optional[Version],
47
47
  int]]:
48
48
  _LOG.log(logging.NOTSET, 'entering %i branches...', len(commit.parents))
49
- results = []
49
+ results: t.List[t.Tuple[
50
+ t.Optional[git.objects.Commit], t.Optional[git.TagReference], Version, int]] = []
50
51
  main_commit_distance = None
51
52
  for parent in commit.parents:
52
53
  try:
@@ -57,7 +58,7 @@ def _latest_git_version_tag_on_branches(
57
58
  if main_commit_distance is None:
58
59
  main_commit_distance = result[3]
59
60
  if result[2] is not None:
60
- results.append(result)
61
+ results.append(result) # type: ignore
61
62
  if not results:
62
63
  if main_commit_distance is None:
63
64
  raise ValueError(f'reached max commit distance {MAX_COMMIT_DISTANCE}'
@@ -73,8 +74,9 @@ MAX_COMMIT_DISTANCE = 999
73
74
 
74
75
 
75
76
  def _latest_git_version_tag(
76
- repo: git.Repo, assume_if_none: bool = False, base_commit: git.objects.Commit = None,
77
- commit_distance: int = 0, skip_commits: t.Set[git.objects.Commit] = None) -> t.Tuple[
77
+ repo: git.Repo, assume_if_none: bool = False,
78
+ base_commit: t.Optional[git.objects.Commit] = None, commit_distance: int = 0,
79
+ skip_commits: t.Optional[t.Set[git.objects.Commit]] = None) -> t.Tuple[
78
80
  t.Optional[git.objects.Commit], t.Optional[git.TagReference], t.Optional[Version], int]:
79
81
  """Return (commit, tag at that commit if any, latest version, distance from the version)."""
80
82
  version_tags = _git_version_tags(repo)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: version-query
3
- Version: 1.4.0rc1
3
+ Version: 1.5.2
4
4
  Summary: Zero-overhead package versioning for Python.
5
5
  Home-page: https://github.com/mbdevpl/version-query
6
6
  Download-URL:
@@ -18,6 +18,7 @@ Classifier: Natural Language :: English
18
18
  Classifier: Operating System :: MacOS :: MacOS X
19
19
  Classifier: Operating System :: Microsoft :: Windows
20
20
  Classifier: Operating System :: POSIX :: Linux
21
+ Classifier: Programming Language :: Python :: 3.8
21
22
  Classifier: Programming Language :: Python :: 3.9
22
23
  Classifier: Programming Language :: Python :: 3.10
23
24
  Classifier: Programming Language :: Python :: 3.11
@@ -27,7 +28,7 @@ Classifier: Topic :: Software Development :: Version Control :: Git
27
28
  Classifier: Topic :: System :: Software Distribution
28
29
  Classifier: Topic :: Utilities
29
30
  Classifier: Typing :: Typed
30
- Requires-Python: >=3.9
31
+ Requires-Python: >=3.8
31
32
  Description-Content-Type: text/x-rst; charset=UTF-8
32
33
  License-File: LICENSE
33
34
  License-File: NOTICE
@@ -62,7 +63,7 @@ Zero-overhead package versioning for Python.
62
63
  :alt: grade from Codacy
63
64
 
64
65
  .. image:: https://img.shields.io/github/license/mbdevpl/version-query.svg
65
- :target: https://github.com/mbdevpl/version-query/blob/v1.4.0rc1/NOTICE
66
+ :target: https://github.com/mbdevpl/version-query/blob/v1.5.2/NOTICE
66
67
  :alt: license
67
68
 
68
69
  Package versioning toolkit for Python which relies on git tags, git history traversal
@@ -470,10 +471,10 @@ using version-query without any issues.
470
471
  Requirements
471
472
  ============
472
473
 
473
- Python version 3.9 or later.
474
+ Python version 3.8 or later.
474
475
 
475
- Python libraries as specified in `requirements.txt <https://github.com/mbdevpl/version-query/blob/v1.4.0rc1/requirements.txt>`_.
476
+ Python libraries as specified in `requirements.txt <https://github.com/mbdevpl/version-query/blob/v1.5.2/requirements.txt>`_.
476
477
 
477
- Building and running tests additionally requires packages listed in `requirements_test.txt <https://github.com/mbdevpl/version-query/blob/v1.4.0rc1/requirements_test.txt>`_.
478
+ Building and running tests additionally requires packages listed in `requirements_test.txt <https://github.com/mbdevpl/version-query/blob/v1.5.2/requirements_test.txt>`_.
478
479
 
479
480
  Tested on Linux, OS X and Windows.
@@ -6,8 +6,13 @@ pyproject.toml
6
6
  requirements.txt
7
7
  requirements_test.txt
8
8
  setup.py
9
- setup_boilerplate.py
10
9
  ./test/__init__.py
10
+ ./test/examples.py
11
+ ./test/test_git.py
12
+ ./test/test_packaging.py
13
+ ./test/test_query.py
14
+ ./test/test_version.py
15
+ ./test/test_with_git_repo.py
11
16
  ./version_query/__init__.py
12
17
  ./version_query/__main__.py
13
18
  ./version_query/_version.py
@@ -19,8 +24,8 @@ setup_boilerplate.py
19
24
  ./version_query/version.py
20
25
  test/examples.py
21
26
  test/test_git.py
27
+ test/test_packaging.py
22
28
  test/test_query.py
23
- test/test_setup.py
24
29
  test/test_version.py
25
30
  test/test_with_git_repo.py
26
31
  version_query.egg-info/PKG-INFO
@@ -1,4 +1,3 @@
1
1
  GitPython~=3.1
2
2
  packaging>=23.0
3
3
  semver~=2.13
4
- setuptools>=67.4
@@ -1,6 +0,0 @@
1
- colorlog ~= 6.7
2
- docutils ~= 0.19
3
- pip >= 23.0
4
- Pygments ~= 2.14
5
- wheel >= 0.40
6
- -r requirements.txt
@@ -1,393 +0,0 @@
1
- """Below code is generic boilerplate and normally should not be changed.
2
-
3
- See the implementation of setup_boilerplate.Package for available options.
4
- Also, some fields have default values. See DEFAULT_* constants below for those values.
5
-
6
- To avoid setup script boilerplate, create "setup.py" file with the minimal contents as given
7
- below and modify it according to the specifics of your package.
8
- Note: string limiters need to be adjusted.
9
-
10
- "" "Setup script." ""
11
-
12
- import setup_boilerplate
13
-
14
-
15
- class Package(setup_boilerplate.Package):
16
- "" "Package metadata." ""
17
-
18
- name = ''
19
- description = ''
20
- url = 'https://github.com/mbdevpl/...'
21
- classifiers = [
22
- 'Development Status :: 1 - Planning',
23
- 'Programming Language :: Python :: 3.7',
24
- 'Programming Language :: Python :: 3.8',
25
- 'Programming Language :: Python :: 3.9',
26
- 'Programming Language :: Python :: 3.10',
27
- 'Programming Language :: Python :: 3 :: Only']
28
- keywords = []
29
-
30
-
31
- if __name__ == '__main__':
32
- Package.setup()
33
- """
34
-
35
- import logging
36
- import pathlib
37
- import runpy
38
- import sys
39
- import typing as t
40
-
41
- import docutils.frontend
42
- import docutils.nodes
43
- import docutils.parsers.rst
44
- import docutils.utils
45
- import setuptools
46
-
47
- __version__ = '2023.03.09'
48
-
49
- _LOG = logging.getLogger(__name__)
50
-
51
- HERE = pathlib.Path(__file__).resolve().parent
52
-
53
- DEFAULT_URL = 'https://github.com/mbdevpl'
54
- DEFAULT_AUTHOR = 'Mateusz Bysiek'
55
- DEFAULT_AUTHOR_EMAIL = 'mateusz.bysiek@gmail.com'
56
- DEFAULT_LICENSE_STR = 'Apache License 2.0'
57
-
58
-
59
- def find_version(
60
- package_name: str, version_module_name: str = '_version',
61
- version_variable_name: str = 'VERSION') -> str:
62
- """Simulate behaviour of "from package_name._version import VERSION", and return VERSION.
63
-
64
- To avoid importing whole package only to read the version, just module containing the version
65
- is imported. Therefore relative imports in that module will break the setup.
66
-
67
- :param package_name: name of the package
68
- :param version_module_name: name of the module containing the version
69
- :param version_variable_name: name of the variable containing the version
70
- :return: version string
71
- """
72
- version_module_path = f'{package_name.replace("-", "_")}/{version_module_name}.py'
73
- version_module_vars = runpy.run_path(version_module_path)
74
- return version_module_vars[version_variable_name]
75
-
76
-
77
- def find_packages(root_directory: str = '.') -> t.List[str]:
78
- """Find packages to pack.
79
-
80
- :param root_directory: directory to start searching from
81
- :return: list of packages
82
- """
83
- exclude = ['test', 'test.*'] if ('bdist_wheel' in sys.argv or 'bdist' in sys.argv) else []
84
- packages_list = setuptools.find_packages(root_directory, exclude=exclude)
85
- return packages_list
86
-
87
-
88
- def parse_requirements(
89
- requirements_path: str = 'requirements.txt') -> t.List[str]:
90
- """Read contents of requirements.txt file and return data from its relevant lines.
91
-
92
- Only non-empty and non-comment lines are relevant.
93
-
94
- :param requirements_path: path to the requirements file if custom one is used
95
- :return: list of requirements
96
- """
97
- requirements = []
98
- with HERE.joinpath(requirements_path).open(encoding='utf-8') as requirements_file:
99
- for requirement in [line.strip() for line in requirements_file.read().splitlines()]:
100
- if not requirement or requirement.startswith('#'):
101
- continue
102
- requirements.append(requirement)
103
- return requirements
104
-
105
-
106
- def partition_version_classifiers(
107
- classifiers: t.Sequence[str], version_prefix: str = 'Programming Language :: Python :: ',
108
- only_suffix: str = ' :: Only'
109
- ) -> t.Tuple[t.List[t.Tuple[int, ...]], t.List[t.Tuple[int, ...]]]:
110
- """Find version number classifiers in given list and partition them into 2 groups.
111
-
112
- The 2 groups being:
113
- 1. minimum versions, any version at least as high this is compatible
114
- 2. only versions, this specific version is compatible, but nothing is said about other versions
115
-
116
- :param classifiers: sequence of trove classifiers to be analysed
117
- :param version_prefix: prefix that identifies a version classifier
118
- :param only_suffix: prefix that identifies an "only version" classifier
119
- :return: parsed and partitioned version tuples
120
- """
121
- versions_min: t.List[t.Tuple[int, ...]] = []
122
- versions_only: t.List[t.Tuple[int, ...]] = []
123
- for classifier in classifiers:
124
- version = classifier.replace(version_prefix, '')
125
- versions = versions_min
126
- if version.endswith(only_suffix):
127
- version = version.replace(only_suffix, '')
128
- versions = versions_only
129
- try:
130
- versions.append(tuple(int(_) for _ in version.split('.')))
131
- except ValueError:
132
- pass
133
- return versions_min, versions_only
134
-
135
-
136
- def find_required_python_version(
137
- classifiers: t.Sequence[str], version_prefix: str = 'Programming Language :: Python :: ',
138
- only_suffix: str = ' :: Only') -> t.Optional[str]:
139
- """Determine the minimum required Python version.
140
-
141
- :param classifiers: sequence of trove classifiers to be analysed
142
- :param version_prefix: prefix that identifies a version classifier
143
- :param only_suffix: prefix that identifies an "only version" classifier
144
- :return: minimum required Python version string, if any
145
- """
146
- versions_min, versions_only = partition_version_classifiers(
147
- classifiers, version_prefix, only_suffix)
148
- if len(versions_only) > 1:
149
- raise ValueError(f'more than one "{only_suffix}" version encountered in {versions_only}')
150
- only_version = None
151
- if len(versions_only) == 1:
152
- only_version = versions_only[0]
153
- for version in versions_min:
154
- if version[:len(only_version)] != only_version:
155
- raise ValueError(f'the "{only_suffix}" version {only_version}'
156
- f' is inconsistent with version {version}')
157
- min_supported_version = None
158
- for version in versions_min:
159
- if min_supported_version is None or \
160
- (len(version) >= len(min_supported_version) and version < min_supported_version):
161
- min_supported_version = version
162
- if min_supported_version is None:
163
- if only_version is not None:
164
- return '.'.join([str(_) for _ in only_version])
165
- else:
166
- return '>=' + '.'.join([str(_) for _ in min_supported_version])
167
- return None
168
-
169
-
170
- def parse_rst(text: str) -> docutils.nodes.document:
171
- """Parse text assuming it's an RST markup."""
172
- parser = docutils.parsers.rst.Parser()
173
- components = (docutils.parsers.rst.Parser,)
174
- settings = docutils.frontend.OptionParser(components=components).get_default_values()
175
- document = docutils.utils.new_document('<rst-doc>', settings=settings)
176
- parser.parse(text, document)
177
- return document
178
-
179
-
180
- class RelativeRefFinder(docutils.nodes.NodeVisitor):
181
- """Find all relative references in a given docutils document that point to existing files."""
182
-
183
- def __init__(self, root_dir: pathlib.Path, *args, **kwargs):
184
- """Initialize the RelativeRefFinder object."""
185
- super().__init__(*args, **kwargs)
186
- self.root_dir = root_dir
187
- self.references: t.List[docutils.nodes.reference] = []
188
-
189
- def visit_reference(self, node: docutils.nodes.reference) -> None:
190
- """Call for "reference" nodes."""
191
- assert isinstance(node, docutils.nodes.TextElement), type(node)
192
- _LOG.debug('RelativeRefFinder: examining reference %s', node)
193
- if len(node.children) != 1 or 'refuri' not in node.attributes \
194
- or node.attributes['refuri'].startswith(('http://', 'https://')):
195
- return
196
- # _LOG.debug(' RelativeRefFinder: reference passed initial check')
197
- path = pathlib.Path(node.attributes['refuri'])
198
- if path.is_absolute():
199
- return
200
- if '#' in path.name:
201
- # reference points to a section in a file
202
- # we ignore the section part when checking if file exists
203
- path = path.with_name(path.name[:path.name.index('#')])
204
- try:
205
- self.root_dir.joinpath(path).resolve(strict=True)
206
- except FileNotFoundError:
207
- return
208
- _LOG.debug('RelativeRefFinder: reference points to existing file')
209
- self.references.append(node)
210
-
211
- def unknown_visit(self, node: docutils.nodes.Node) -> None:
212
- """Call for unknown node types."""
213
- return
214
-
215
-
216
- def resolve_relative_rst_links(text: str, base_link: str) -> str:
217
- """Resolve all relative links in a given string representing an RST document.
218
-
219
- Links are resolved only if they point to files existing in the project's working directory.
220
-
221
- All links of form `link`_ become `link <absolute_link>`_.
222
-
223
- And all link of the form :target: link become :target: absolute_link.
224
-
225
- Where absolute_link is made by concatenating base_link to link with no separator added
226
- in between. So in most cases base_link should end with a slash.
227
- """
228
- document = parse_rst(text)
229
- finder = RelativeRefFinder(HERE, document)
230
- document.walk(finder)
231
- for target in finder.references:
232
- _LOG.info('resolve_relative_rst_links: resolving reference %s', target)
233
- assert isinstance(target, docutils.nodes.TextElement), type(target)
234
- refuri = target.attributes['refuri']
235
- if 'name' in target.attributes:
236
- name = target.attributes['name']
237
- if name == refuri:
238
- old_link = f'`<{refuri}>`_'
239
- else:
240
- old_link = f'`{name} <{refuri}>`_'
241
- new_link = f'`{name} <{base_link}{refuri}>`_'
242
- else:
243
- old_link = f' :target: {refuri}'
244
- new_link = f' :target: {base_link}{refuri}'
245
- text = text.replace(old_link, new_link)
246
- _LOG.info('resolve_relative_rst_links: replaced "%s" with "%s"', old_link, new_link)
247
- return text
248
-
249
-
250
- class Package:
251
- """Default metadata and behaviour for a Python package setup script."""
252
-
253
- root_directory: str = '.'
254
- """Root directory of the source code of the package, relative to the setup.py file location."""
255
-
256
- name: str
257
- """Package name."""
258
-
259
- version: str
260
- """Package version as string.
261
-
262
- If None, it will be obtained from "package_name._version.VERSION" variable.
263
- """
264
-
265
- description: str
266
- """One line of text that shortly describes the package."""
267
-
268
- long_description: str
269
- """Full description of the package, can be arbitrarily long.
270
-
271
- If not set, it will be generated from readme.
272
- """
273
-
274
- long_description_content_type: str
275
- """Content type of the long description.
276
-
277
- Usually one of 'text/x-rst', 'text/markdown' or 'text/plain'.
278
-
279
- If not set, it will be set according to readme file extension.
280
- For this field to be automatically set, also long_description field cannot be set.
281
- """
282
-
283
- url: str = DEFAULT_URL
284
- download_url: str = ''
285
- author: str = DEFAULT_AUTHOR
286
- author_email: str = DEFAULT_AUTHOR_EMAIL
287
- maintainer: str
288
- maintainer_email: str
289
- license_str: str = DEFAULT_LICENSE_STR
290
-
291
- classifiers: t.List[str] = []
292
- """List of trove classifiers for the package.
293
-
294
- List of valid classifiers: https://pypi.org/pypi?:action=list_classifiers
295
- """
296
-
297
- keywords: t.List[str] = []
298
-
299
- packages: t.List[str]
300
- """List of Python packages to be included in the distributed file.
301
-
302
- Usually it's just one package per distributed file, but any number is supported.
303
-
304
- If not set, determined with help of setuptools.
305
- """
306
-
307
- package_data: t.Dict[str, t.List[str]] = {}
308
- exclude_package_data: t.Dict[str, t.List[str]] = {}
309
-
310
- install_requires: t.Optional[t.List[str]] = None
311
- """If None, determined using requirements.txt."""
312
-
313
- extras_require: t.Mapping[str, t.List[str]] = {}
314
- """A dictionary containing entries of type 'some_feature': ['requirement1', 'requirement2']."""
315
-
316
- python_requires: t.Optional[str] = None
317
- """If None, determined from provided classifiers."""
318
-
319
- entry_points: t.Mapping[str, t.List[str]] = {}
320
- """A dictionary used to enable automatic creation of console scripts, gui scripts and plugins.
321
-
322
- Example entry:
323
- 'console_scripts': ['script_name = package.subpackage:function']
324
- """
325
-
326
- test_suite: str = 'test'
327
-
328
- @classmethod
329
- def try_fields(cls, *names) -> t.Optional[t.Any]:
330
- """Return first existing of given class field names."""
331
- for name in names:
332
- if hasattr(cls, name):
333
- return getattr(cls, name)
334
- raise AttributeError((cls, names))
335
-
336
- @classmethod
337
- def parse_readme(cls, readme_filename: str = 'README.rst',
338
- encoding: str = 'utf-8') -> t.Tuple[str, str]:
339
- """Parse readme and resolve relative links in it if it is feasible.
340
-
341
- Links are resolved if readme is in rst format and the package is hosted on GitHub.
342
- """
343
- readme_path = HERE.joinpath(readme_filename)
344
- with readme_path.open(encoding=encoding) as readme_file:
345
- long_description: str = readme_file.read()
346
-
347
- if readme_path.suffix.lower() == '.rst' and cls.url.startswith('https://github.com/'):
348
- base_url = f'{cls.url}/blob/v{cls.version}/'
349
- long_description = resolve_relative_rst_links(long_description, base_url)
350
-
351
- long_description_content_type = {'.rst': 'text/x-rst', '.md': 'text/markdown'}.get(
352
- readme_path.suffix.lower(), 'text/plain')
353
- long_description_content_type += '; charset=UTF-8'
354
-
355
- return long_description, long_description_content_type
356
-
357
- @classmethod
358
- def prepare(cls) -> None:
359
- """Fill in possibly missing package metadata."""
360
- if not hasattr(cls, 'version'):
361
- cls.version = find_version(cls.name)
362
- if not hasattr(cls, 'long_description'):
363
- cls.long_description, cls.long_description_content_type = cls.parse_readme()
364
- if not hasattr(cls, 'maintainer'):
365
- cls.maintainer = cls.author
366
- if not hasattr(cls, 'maintainer_email'):
367
- cls.maintainer_email = cls.author_email
368
- if not hasattr(cls, 'packages'):
369
- cls.packages = find_packages(cls.root_directory)
370
- if cls.install_requires is None:
371
- cls.install_requires = parse_requirements()
372
- if cls.python_requires is None:
373
- cls.python_requires = find_required_python_version(cls.classifiers)
374
-
375
- @classmethod
376
- def setup(cls) -> None:
377
- """Call setuptools.setup with correct arguments."""
378
- cls.prepare()
379
- setuptools.setup(
380
- name=cls.name, version=cls.version, description=cls.description,
381
- long_description=cls.long_description,
382
- long_description_content_type=cls.long_description_content_type,
383
- url=cls.url, download_url=cls.download_url,
384
- author=cls.author, author_email=cls.author_email,
385
- maintainer=cls.maintainer,
386
- maintainer_email=cls.maintainer_email,
387
- license=cls.license_str, classifiers=cls.classifiers, keywords=cls.keywords,
388
- packages=cls.packages, package_dir={'': cls.root_directory},
389
- include_package_data=True,
390
- package_data=cls.package_data, exclude_package_data=cls.exclude_package_data,
391
- install_requires=cls.install_requires, extras_require=cls.extras_require,
392
- python_requires=cls.python_requires,
393
- entry_points=cls.entry_points, test_suite=cls.test_suite)
@@ -1,364 +0,0 @@
1
- """Tests for setup scripts."""
2
-
3
- import importlib
4
- import itertools
5
- import logging
6
- import os
7
- import pathlib
8
- import runpy
9
- import subprocess
10
- import sys
11
- import tempfile
12
- import types
13
- import typing as t
14
- import unittest
15
-
16
- __version__ = '2023.03.09'
17
-
18
- _LOG = logging.getLogger(__name__)
19
-
20
-
21
- def expand_args_by_globbing_items(*args: str) -> t.Tuple[str, ...]:
22
- """Expand a list of glob expressions."""
23
- cwd = pathlib.Path.cwd()
24
- expanded_args = []
25
- for arg in args:
26
- if '*' not in arg:
27
- expanded_args.append(arg)
28
- continue
29
- expanded_arg = [str(_.relative_to(cwd)) for _ in cwd.glob(arg)]
30
- assert expanded_arg, arg
31
- _LOG.debug('expanded arg "%s" to %s', arg, expanded_arg)
32
- expanded_args += expanded_arg
33
- _LOG.debug('expanded args to %s', expanded_args)
34
- return tuple(expanded_args)
35
-
36
-
37
- def run_program(*args, glob: bool = False) -> None:
38
- """Run subprocess with given args. Use path globbing for each arg that contains an asterisk."""
39
- if glob:
40
- args = expand_args_by_globbing_items(*args)
41
- try:
42
- subprocess.run(args, check=True)
43
- except subprocess.CalledProcessError as err:
44
- raise AssertionError(f'execution of {args} failed') from err
45
-
46
-
47
- def run_pip(*args, **kwargs) -> None:
48
- run_program(sys.executable, '-m', 'pip', *args, **kwargs)
49
-
50
-
51
- def run_module(name: str, *args, run_name: str = '__main__') -> None:
52
- backup_sys_argv = sys.argv
53
- sys.argv = [name + '.py'] + list(args)
54
- runpy.run_module(name, run_name=run_name)
55
- sys.argv = backup_sys_argv
56
-
57
-
58
- def import_module(name: str = 'setup') -> types.ModuleType:
59
- setup_module = importlib.import_module(name)
60
- return setup_module
61
-
62
-
63
- def import_module_member(module_name: str, member_name: str) -> t.Any:
64
- module = import_module(module_name)
65
- return getattr(module, member_name)
66
-
67
-
68
- CLASSIFIERS_LICENSES = (
69
- 'License :: OSI Approved :: Python License (CNRI Python License)',
70
- 'License :: OSI Approved :: Python Software Foundation License',
71
- 'License :: Other/Proprietary License',
72
- 'License :: Public Domain')
73
-
74
- CLASSIFIERS_PYTHON_VERSIONS = tuple("""Programming Language :: Python
75
- Programming Language :: Python :: 3
76
- Programming Language :: Python :: 3.0
77
- Programming Language :: Python :: 3.5
78
- Programming Language :: Python :: 3 :: Only""".splitlines())
79
-
80
- CLASSIFIERS_PYTHON_IMPLEMENTATIONS = tuple("""Programming Language :: Python :: Implementation
81
- Programming Language :: Python :: Implementation :: CPython
82
- Programming Language :: Python :: Implementation :: Jython
83
- Programming Language :: Python :: Implementation :: PyPy
84
- Programming Language :: Python :: Implementation :: Stackless""".splitlines())
85
-
86
- CLASSIFIERS_VARIOUS = (
87
- 'Framework :: IPython',
88
- 'Topic :: Scientific/Engineering',
89
- 'Topic :: Sociology',
90
- 'Topic :: Security :: Cryptography',
91
- 'Topic :: Software Development :: Libraries :: Python Modules',
92
- 'Topic :: Software Development :: Version Control :: Git',
93
- 'Topic :: System',
94
- 'Topic :: Utilities')
95
-
96
- CLASSIFIERS_LICENSES_TUPLES = tuple((_,) for _ in CLASSIFIERS_LICENSES) + ((),)
97
-
98
- CLASSIFIERS_PYTHON_VERSIONS_COMBINATIONS = tuple((_,) for _ in CLASSIFIERS_PYTHON_VERSIONS)
99
-
100
- CLASSIFIERS_PYTHON_IMPLEMENTATIONS_TUPLES = tuple((_,) for _ in CLASSIFIERS_PYTHON_IMPLEMENTATIONS)
101
-
102
- CLASSIFIERS_VARIOUS_COMBINATIONS = tuple(itertools.combinations(
103
- CLASSIFIERS_VARIOUS, len(CLASSIFIERS_VARIOUS) - 1)) + (CLASSIFIERS_VARIOUS,)
104
-
105
- ALL_CLASSIFIERS_VARIANTS = [
106
- licenses + versions + implementations + various
107
- for licenses in CLASSIFIERS_LICENSES_TUPLES
108
- for versions in CLASSIFIERS_PYTHON_VERSIONS_COMBINATIONS
109
- for implementations in CLASSIFIERS_PYTHON_IMPLEMENTATIONS_TUPLES
110
- for various in CLASSIFIERS_VARIOUS_COMBINATIONS]
111
-
112
- LINK_EXAMPLES = [
113
- (None, 'setup.py', True), ('this file', 'setup.py', True), (None, 'test/test_setup.py', True),
114
- (None, 'test/test_setup.py#L98', True), ('line 5 of this file', 'setup.py#L5', True),
115
- (None, 'http://site.com', False), (None, '../something/else', False), (None, 'no.thing', False),
116
- (None, '/my/abs/path', False), ('test dir', 'test', True)]
117
-
118
-
119
- def get_package_folder_name():
120
- """Attempt to guess the built package name."""
121
- cwd = pathlib.Path.cwd()
122
- directories = [
123
- path for path in cwd.iterdir() if pathlib.Path(cwd, path).is_dir()
124
- and pathlib.Path(cwd, path, '__init__.py').is_file() and path.name != 'test']
125
- assert len(directories) == 1, directories
126
- return directories[0].name
127
-
128
-
129
- class UnitTests(unittest.TestCase):
130
- """Test basic functionalities of the setup boilerplate."""
131
-
132
- def test_find_version(self):
133
- find_version = import_module_member('setup_boilerplate', 'find_version')
134
- result = find_version(get_package_folder_name())
135
- self.assertIsInstance(result, str)
136
-
137
- def test_find_packages(self):
138
- find_packages = import_module_member('setup_boilerplate', 'find_packages')
139
- results = find_packages()
140
- self.assertIsInstance(results, list)
141
- for result in results:
142
- self.assertIsInstance(result, str)
143
-
144
- def test_requirements(self):
145
- parse_requirements = import_module_member('setup_boilerplate', 'parse_requirements')
146
- results = parse_requirements()
147
- self.assertIsInstance(results, list)
148
- self.assertTrue(all(isinstance(result, str) for result in results), msg=results)
149
-
150
- def test_requirements_empty(self):
151
- parse_requirements = import_module_member('setup_boilerplate', 'parse_requirements')
152
- with tempfile.NamedTemporaryFile('w', delete=False) as reqs_file:
153
- pass
154
- results = parse_requirements(reqs_file.name)
155
- self.assertIsInstance(results, list)
156
- self.assertEqual(len(results), 0)
157
- os.remove(reqs_file.name)
158
-
159
- def test_requirements_comments(self):
160
- parse_requirements = import_module_member('setup_boilerplate', 'parse_requirements')
161
- reqs = ['# comment', 'numpy', '', '# another comment', 'scipy', '', '# one more comment']
162
- with tempfile.NamedTemporaryFile('w', delete=False) as reqs_file:
163
- for req in reqs:
164
- print(req, file=reqs_file)
165
- results = parse_requirements(reqs_file.name)
166
- self.assertIsInstance(results, list)
167
- self.assertGreater(len(results), 0)
168
- self.assertLess(len(results), len(reqs))
169
- os.remove(reqs_file.name)
170
-
171
- def test_python_versions(self):
172
- find_required_python_version = import_module_member(
173
- 'setup_boilerplate', 'find_required_python_version')
174
- for variant in ALL_CLASSIFIERS_VARIANTS:
175
- with self.subTest(variant=variant):
176
- result = find_required_python_version(variant)
177
- if result is not None:
178
- self.assertIsInstance(result, str)
179
-
180
- def test_python_versions_combined(self):
181
- find_required_python_version = import_module_member(
182
- 'setup_boilerplate', 'find_required_python_version')
183
- classifiers = [
184
- 'Programming Language :: Python :: 3 :: Only',
185
- 'Programming Language :: Python :: 3.5']
186
- req = find_required_python_version(classifiers)
187
- self.assertEqual(req, '>=3.5')
188
-
189
- def test_python_versions_reversed(self):
190
- find_required_python_version = import_module_member(
191
- 'setup_boilerplate', 'find_required_python_version')
192
- classifiers = [
193
- 'Programming Language :: Python :: 3.4',
194
- 'Programming Language :: Python :: 3.5',
195
- 'Programming Language :: Python :: 3.6']
196
- req = find_required_python_version(classifiers)
197
- self.assertEqual(req, '>=3.4')
198
- req = find_required_python_version(reversed(classifiers))
199
- self.assertEqual(req, '>=3.4')
200
-
201
- def test_python_versions_none(self):
202
- find_required_python_version = import_module_member(
203
- 'setup_boilerplate', 'find_required_python_version')
204
- result = find_required_python_version([])
205
- self.assertIsNone(result)
206
-
207
- def test_python_versions_many_only(self):
208
- find_required_python_version = import_module_member(
209
- 'setup_boilerplate', 'find_required_python_version')
210
- classifiers = [
211
- 'Programming Language :: Python :: 2 :: Only',
212
- 'Programming Language :: Python :: 3 :: Only']
213
- with self.assertRaises(ValueError):
214
- find_required_python_version(classifiers)
215
-
216
- def test_python_versions_conflict(self):
217
- find_required_python_version = import_module_member(
218
- 'setup_boilerplate', 'find_required_python_version')
219
- classifier_variants = [
220
- ['Programming Language :: Python :: 2.7',
221
- 'Programming Language :: Python :: 3 :: Only'],
222
- ['Programming Language :: Python :: 2 :: Only',
223
- 'Programming Language :: Python :: 3.0']]
224
- for classifiers in classifier_variants:
225
- with self.assertRaises(ValueError):
226
- find_required_python_version(classifiers)
227
-
228
-
229
- class PackageTests(unittest.TestCase):
230
- """Test methods of Package class."""
231
-
232
- def test_try_fields(self):
233
- package = import_module_member('setup_boilerplate', 'Package')
234
-
235
- class Package(package): # pylint: disable=too-few-public-methods
236
- name = 'package name'
237
- description = 'package description'
238
- self.assertEqual(Package.try_fields('name', 'description'), 'package name')
239
- self.assertEqual(Package.try_fields('bad_field', 'description'), 'package description')
240
- with self.assertRaises(AttributeError):
241
- self.assertIsNone(Package.try_fields())
242
- with self.assertRaises(AttributeError):
243
- Package.try_fields('bad_field', 'another_bad_field')
244
-
245
- def test_parse_readme(self):
246
- package = import_module_member('setup_boilerplate', 'Package')
247
-
248
- class Package(package): # pylint: disable=too-few-public-methods
249
- name = 'package name'
250
- description = 'package description'
251
- version = '1.2.3.4'
252
- url = 'https://github.com/example'
253
-
254
- with tempfile.NamedTemporaryFile('w', suffix='.md', delete=False) as temp_file:
255
- temp_file.write('test test test')
256
- result, content_type = Package.parse_readme(temp_file.name)
257
- os.remove(temp_file.name)
258
- self.assertIsInstance(result, str)
259
- self.assertIsInstance(content_type, str)
260
-
261
- prefix = 'https://github.com/example/blob/v1.2.3.4/'
262
- for name, link, done in LINK_EXAMPLES:
263
- name = '' if name is None else name + ' '
264
- text = f'Please see `{name}<{link}>`_ for details.'
265
- with tempfile.NamedTemporaryFile('w', suffix='.rst', delete=False) as temp_file:
266
- temp_file.write(text)
267
- result, content_type = Package.parse_readme(temp_file.name)
268
- os.remove(temp_file.name)
269
- self.assertIsInstance(result, str)
270
- self.assertIsInstance(content_type, str)
271
- if not done:
272
- self.assertEqual(result, text)
273
- continue
274
- if name == '':
275
- name = link + ' '
276
- self.assertIn(f'`{name}<{prefix}{link}>`_', result)
277
-
278
- def test_prepare(self):
279
- package = import_module_member('setup_boilerplate', 'Package')
280
-
281
- version_ = '1.2.3.4.5.6.7'
282
- long_description_ = 'long package description'
283
-
284
- class Package(package): # pylint: disable=too-few-public-methods, missing-docstring
285
- name = 'package name'
286
- version = version_
287
- description = 'package description'
288
- long_description = long_description_
289
- packages = []
290
- install_requires = []
291
- python_requires = ''
292
-
293
- self.assertEqual(Package.version, version_)
294
- self.assertEqual(Package.long_description, long_description_)
295
- Package.prepare()
296
- self.assertEqual(Package.version, version_)
297
- self.assertEqual(Package.long_description, long_description_)
298
-
299
- del Package.long_description
300
- del Package.packages
301
- del Package.install_requires
302
- del Package.python_requires
303
- Package.prepare()
304
-
305
- del Package.version
306
- with self.assertRaises(FileNotFoundError):
307
- Package.prepare()
308
-
309
-
310
- @unittest.skipUnless(os.environ.get('TEST_PACKAGING') or os.environ.get('CI'),
311
- 'skipping packaging tests for actual package')
312
- class IntegrationTests(unittest.TestCase):
313
- """Test if the boilerplate can actually create a valid package."""
314
-
315
- pkg_name = get_package_folder_name()
316
-
317
- def test_build_binary(self):
318
- run_module('setup', 'bdist')
319
- self.assertTrue(os.path.isdir('dist'))
320
-
321
- def test_build_wheel(self):
322
- run_module('setup', 'bdist_wheel')
323
- self.assertTrue(os.path.isdir('dist'))
324
-
325
- def test_build_source(self):
326
- run_module('setup', 'sdist', '--formats=gztar,zip')
327
- self.assertTrue(os.path.isdir('dist'))
328
-
329
- def test_install_code(self):
330
- with tempfile.TemporaryDirectory() as temporary_folder:
331
- run_pip('install', '--prefix', temporary_folder, '.')
332
- self.assertFalse(pathlib.Path(temporary_folder).exists())
333
-
334
- def test_install_source_tar(self):
335
- find_version = import_module_member('setup_boilerplate', 'find_version')
336
- version = find_version(self.pkg_name)
337
- with tempfile.TemporaryDirectory() as temporary_folder:
338
- run_pip(
339
- 'install', '--prefix', temporary_folder, f'dist/*-{version}.tar.gz', glob=True)
340
- self.assertFalse(pathlib.Path(temporary_folder).exists())
341
-
342
- def test_install_source_zip(self):
343
- find_version = import_module_member('setup_boilerplate', 'find_version')
344
- version = find_version(self.pkg_name)
345
- with tempfile.TemporaryDirectory() as temporary_folder:
346
- run_pip('install', '--prefix', temporary_folder, f'dist/*-{version}.zip', glob=True)
347
- self.assertFalse(pathlib.Path(temporary_folder).exists())
348
-
349
- def test_install_wheel(self):
350
- find_version = import_module_member('setup_boilerplate', 'find_version')
351
- version = find_version(self.pkg_name)
352
- with tempfile.TemporaryDirectory() as temporary_folder:
353
- run_pip(
354
- 'install', '--prefix', temporary_folder, f'dist/*-{version}-*.whl', glob=True)
355
- self.assertFalse(pathlib.Path(temporary_folder).exists())
356
-
357
- def test_pip_error(self):
358
- with self.assertRaises(AssertionError):
359
- run_pip('wrong_pip_command')
360
-
361
- def test_setup_do_nothing_or_error(self):
362
- run_module('setup', 'wrong_setup_command', run_name='__not_main__')
363
- with self.assertRaises(SystemExit):
364
- run_module('setup', 'wrong_setup_command')
File without changes
File without changes