py2docfx 0.1.9.dev1917798__py3-none-any.whl → 0.1.9.dev1926139__py3-none-any.whl

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 (79) hide show
  1. py2docfx/__main__.py +77 -22
  2. py2docfx/convert_prepare/environment.py +34 -13
  3. py2docfx/convert_prepare/generate_document.py +12 -5
  4. py2docfx/convert_prepare/get_source.py +5 -1
  5. py2docfx/convert_prepare/git.py +24 -20
  6. py2docfx/convert_prepare/package_info.py +15 -9
  7. py2docfx/convert_prepare/pip_utils.py +29 -8
  8. py2docfx/convert_prepare/post_process/merge_toc.py +5 -1
  9. py2docfx/convert_prepare/sphinx_caller.py +31 -14
  10. py2docfx/convert_prepare/tests/test_generate_document.py +2 -0
  11. py2docfx/convert_prepare/tests/test_sphinx_caller.py +2 -0
  12. py2docfx/convert_prepare/tests/utils.py +11 -0
  13. py2docfx/docfx_yaml/build_finished.py +11 -3
  14. py2docfx/docfx_yaml/convert_class.py +4 -2
  15. py2docfx/docfx_yaml/convert_enum.py +4 -2
  16. py2docfx/docfx_yaml/convert_module.py +4 -2
  17. py2docfx/docfx_yaml/convert_package.py +4 -2
  18. py2docfx/docfx_yaml/logger.py +68 -0
  19. py2docfx/docfx_yaml/process_doctree.py +6 -4
  20. py2docfx/docfx_yaml/translator.py +5 -7
  21. py2docfx/docfx_yaml/writer.py +10 -4
  22. py2docfx/docfx_yaml/yaml_builder.py +0 -1
  23. py2docfx/venv/basevenv/Lib/site-packages/setuptools/build_meta.py +2 -2
  24. py2docfx/venv/basevenv/Lib/site-packages/setuptools/command/bdist_egg.py +1 -1
  25. py2docfx/venv/basevenv/Lib/site-packages/setuptools/command/bdist_wheel.py +25 -39
  26. py2docfx/venv/basevenv/Lib/site-packages/setuptools/command/build_ext.py +2 -2
  27. py2docfx/venv/basevenv/Lib/site-packages/setuptools/command/build_py.py +9 -14
  28. py2docfx/venv/basevenv/Lib/site-packages/setuptools/command/easy_install.py +2 -2
  29. py2docfx/venv/basevenv/Lib/site-packages/setuptools/command/editable_wheel.py +2 -2
  30. py2docfx/venv/basevenv/Lib/site-packages/setuptools/command/egg_info.py +3 -2
  31. py2docfx/venv/basevenv/Lib/site-packages/setuptools/command/install_egg_info.py +2 -2
  32. py2docfx/venv/basevenv/Lib/site-packages/setuptools/command/saveopts.py +2 -2
  33. py2docfx/venv/basevenv/Lib/site-packages/setuptools/command/sdist.py +2 -2
  34. py2docfx/venv/basevenv/Lib/site-packages/setuptools/command/setopt.py +1 -1
  35. py2docfx/venv/basevenv/Lib/site-packages/setuptools/config/pyprojecttoml.py +0 -13
  36. py2docfx/venv/basevenv/Lib/site-packages/setuptools/dist.py +3 -2
  37. py2docfx/venv/basevenv/Lib/site-packages/setuptools/monkey.py +3 -3
  38. py2docfx/venv/basevenv/Lib/site-packages/setuptools/msvc.py +11 -11
  39. py2docfx/venv/basevenv/Lib/site-packages/setuptools/tests/config/test_pyprojecttoml.py +0 -7
  40. py2docfx/venv/basevenv/Lib/site-packages/setuptools/tests/test_core_metadata.py +168 -72
  41. py2docfx/venv/basevenv/Lib/site-packages/setuptools/unicode_utils.py +3 -3
  42. py2docfx/venv/basevenv/Lib/site-packages/wheel/__init__.py +1 -1
  43. py2docfx/venv/basevenv/Lib/site-packages/wheel/cli/convert.py +1 -2
  44. py2docfx/venv/venv1/Lib/site-packages/jwt/__init__.py +3 -2
  45. py2docfx/venv/venv1/Lib/site-packages/jwt/algorithms.py +31 -16
  46. py2docfx/venv/venv1/Lib/site-packages/jwt/api_jws.py +19 -8
  47. py2docfx/venv/venv1/Lib/site-packages/jwt/api_jwt.py +75 -19
  48. py2docfx/venv/venv1/Lib/site-packages/jwt/exceptions.py +8 -0
  49. py2docfx/venv/venv1/Lib/site-packages/jwt/help.py +4 -1
  50. py2docfx/venv/venv1/Lib/site-packages/jwt/jwks_client.py +4 -2
  51. py2docfx/venv/venv1/Lib/site-packages/jwt/utils.py +7 -10
  52. py2docfx/venv/venv1/Lib/site-packages/msal/application.py +1 -1
  53. py2docfx/venv/venv1/Lib/site-packages/msal/managed_identity.py +5 -3
  54. py2docfx/venv/venv1/Lib/site-packages/setuptools/build_meta.py +2 -2
  55. py2docfx/venv/venv1/Lib/site-packages/setuptools/command/bdist_egg.py +1 -1
  56. py2docfx/venv/venv1/Lib/site-packages/setuptools/command/bdist_wheel.py +25 -39
  57. py2docfx/venv/venv1/Lib/site-packages/setuptools/command/build_ext.py +2 -2
  58. py2docfx/venv/venv1/Lib/site-packages/setuptools/command/build_py.py +9 -14
  59. py2docfx/venv/venv1/Lib/site-packages/setuptools/command/easy_install.py +2 -2
  60. py2docfx/venv/venv1/Lib/site-packages/setuptools/command/editable_wheel.py +2 -2
  61. py2docfx/venv/venv1/Lib/site-packages/setuptools/command/egg_info.py +3 -2
  62. py2docfx/venv/venv1/Lib/site-packages/setuptools/command/install_egg_info.py +2 -2
  63. py2docfx/venv/venv1/Lib/site-packages/setuptools/command/saveopts.py +2 -2
  64. py2docfx/venv/venv1/Lib/site-packages/setuptools/command/sdist.py +2 -2
  65. py2docfx/venv/venv1/Lib/site-packages/setuptools/command/setopt.py +1 -1
  66. py2docfx/venv/venv1/Lib/site-packages/setuptools/config/pyprojecttoml.py +0 -13
  67. py2docfx/venv/venv1/Lib/site-packages/setuptools/dist.py +3 -2
  68. py2docfx/venv/venv1/Lib/site-packages/setuptools/monkey.py +3 -3
  69. py2docfx/venv/venv1/Lib/site-packages/setuptools/msvc.py +11 -11
  70. py2docfx/venv/venv1/Lib/site-packages/setuptools/tests/config/test_pyprojecttoml.py +0 -7
  71. py2docfx/venv/venv1/Lib/site-packages/setuptools/tests/test_core_metadata.py +168 -72
  72. py2docfx/venv/venv1/Lib/site-packages/setuptools/unicode_utils.py +3 -3
  73. py2docfx/venv/venv1/Lib/site-packages/wheel/__init__.py +1 -1
  74. py2docfx/venv/venv1/Lib/site-packages/wheel/cli/convert.py +1 -2
  75. {py2docfx-0.1.9.dev1917798.dist-info → py2docfx-0.1.9.dev1926139.dist-info}/METADATA +1 -1
  76. {py2docfx-0.1.9.dev1917798.dist-info → py2docfx-0.1.9.dev1926139.dist-info}/RECORD +79 -77
  77. {py2docfx-0.1.9.dev1917798.dist-info → py2docfx-0.1.9.dev1926139.dist-info}/WHEEL +1 -1
  78. /py2docfx/convert_prepare/conf_templates/{master_doc.rst_t → root_doc.rst_t} +0 -0
  79. {py2docfx-0.1.9.dev1917798.dist-info → py2docfx-0.1.9.dev1926139.dist-info}/top_level.txt +0 -0
@@ -39,7 +39,7 @@ class build_py(orig.build_py):
39
39
 
40
40
  distribution: Distribution # override distutils.dist.Distribution with setuptools.dist.Distribution
41
41
  editable_mode: bool = False
42
- existing_egg_info_dir: str | None = None #: Private API, internal use only.
42
+ existing_egg_info_dir: StrPath | None = None #: Private API, internal use only.
43
43
 
44
44
  def finalize_options(self):
45
45
  orig.build_py.finalize_options(self)
@@ -47,7 +47,6 @@ class build_py(orig.build_py):
47
47
  self.exclude_package_data = self.distribution.exclude_package_data or {}
48
48
  if 'data_files' in self.__dict__:
49
49
  del self.__dict__['data_files']
50
- self.__updated_files = []
51
50
 
52
51
  def copy_file( # type: ignore[override] # No overload, no bytes support
53
52
  self,
@@ -89,12 +88,6 @@ class build_py(orig.build_py):
89
88
  return self.data_files
90
89
  return orig.build_py.__getattr__(self, attr)
91
90
 
92
- def build_module(self, module, module_file, package):
93
- outfile, copied = orig.build_py.build_module(self, module, module_file, package)
94
- if copied:
95
- self.__updated_files.append(outfile)
96
- return outfile, copied
97
-
98
91
  def _get_data_files(self):
99
92
  """Generate list of '(package,src_dir,build_dir,filenames)' tuples"""
100
93
  self.analyze_manifest()
@@ -178,17 +171,17 @@ class build_py(orig.build_py):
178
171
  _outf, _copied = self.copy_file(srcfile, target)
179
172
  make_writable(target)
180
173
 
181
- def analyze_manifest(self):
182
- self.manifest_files = mf = {}
174
+ def analyze_manifest(self) -> None:
175
+ self.manifest_files: dict[str, list[str]] = {}
183
176
  if not self.distribution.include_package_data:
184
177
  return
185
- src_dirs = {}
178
+ src_dirs: dict[str, str] = {}
186
179
  for package in self.packages or ():
187
180
  # Locate package source directory
188
181
  src_dirs[assert_relative(self.get_package_dir(package))] = package
189
182
 
190
183
  if (
191
- getattr(self, 'existing_egg_info_dir', None)
184
+ self.existing_egg_info_dir
192
185
  and Path(self.existing_egg_info_dir, "SOURCES.txt").exists()
193
186
  ):
194
187
  egg_info_dir = self.existing_egg_info_dir
@@ -217,9 +210,11 @@ class build_py(orig.build_py):
217
210
  importable = check.importable_subpackage(src_dirs[d], f)
218
211
  if importable:
219
212
  check.warn(importable)
220
- mf.setdefault(src_dirs[d], []).append(path)
213
+ self.manifest_files.setdefault(src_dirs[d], []).append(path)
221
214
 
222
- def _filter_build_files(self, files: Iterable[str], egg_info: str) -> Iterator[str]:
215
+ def _filter_build_files(
216
+ self, files: Iterable[str], egg_info: StrPath
217
+ ) -> Iterator[str]:
223
218
  """
224
219
  ``build_meta`` may try to create egg_info outside of the project directory,
225
220
  and this can be problematic for certain plugins (reported in issue #3500).
@@ -238,7 +238,7 @@ class easy_install(Command):
238
238
  print(f'setuptools {dist.version} from {dist.location} (Python {ver})')
239
239
  raise SystemExit
240
240
 
241
- def finalize_options(self): # noqa: C901 # is too complex (25) # FIXME
241
+ def finalize_options(self) -> None: # noqa: C901 # is too complex (25) # FIXME
242
242
  self.version and self._render_version()
243
243
 
244
244
  py_version = sys.version.split()[0]
@@ -354,7 +354,7 @@ class easy_install(Command):
354
354
  "No urls, filenames, or requirements specified (see --help)"
355
355
  )
356
356
 
357
- self.outputs = []
357
+ self.outputs: list[str] = []
358
358
 
359
359
  @staticmethod
360
360
  def _process_site_dirs(site_dirs):
@@ -779,12 +779,12 @@ def _empty_dir(dir_: _P) -> _P:
779
779
 
780
780
 
781
781
  class _NamespaceInstaller(namespaces.Installer):
782
- def __init__(self, distribution, installation_dir, editable_name, src_root):
782
+ def __init__(self, distribution, installation_dir, editable_name, src_root) -> None:
783
783
  self.distribution = distribution
784
784
  self.src_root = src_root
785
785
  self.installation_dir = installation_dir
786
786
  self.editable_name = editable_name
787
- self.outputs = []
787
+ self.outputs: list[str] = []
788
788
  self.dry_run = False
789
789
 
790
790
  def _get_nspkg_file(self):
@@ -7,6 +7,7 @@ import os
7
7
  import re
8
8
  import sys
9
9
  import time
10
+ from collections.abc import Callable
10
11
 
11
12
  import packaging
12
13
  import packaging.requirements
@@ -330,7 +331,7 @@ class FileList(_FileList):
330
331
  super().__init__(warn, debug_print)
331
332
  self.ignore_egg_info_dir = ignore_egg_info_dir
332
333
 
333
- def process_template_line(self, line):
334
+ def process_template_line(self, line) -> None:
334
335
  # Parse the line: split it up, make sure the right number of words
335
336
  # is there, and return the relevant words. 'action' is always
336
337
  # defined: it's the first word of the line. Which of the other
@@ -338,7 +339,7 @@ class FileList(_FileList):
338
339
  # patterns, (dir and patterns), or (dir_pattern).
339
340
  (action, patterns, dir, dir_pattern) = self._parse_template_line(line)
340
341
 
341
- action_map = {
342
+ action_map: dict[str, Callable] = {
342
343
  'include': self.include,
343
344
  'exclude': self.exclude,
344
345
  'global-include': self.global_include,
@@ -20,13 +20,13 @@ class install_egg_info(namespaces.Installer, Command):
20
20
  def initialize_options(self):
21
21
  self.install_dir = None
22
22
 
23
- def finalize_options(self):
23
+ def finalize_options(self) -> None:
24
24
  self.set_undefined_options('install_lib', ('install_dir', 'install_dir'))
25
25
  ei_cmd = self.get_finalized_command("egg_info")
26
26
  basename = f"{ei_cmd._get_egg_basename()}.egg-info"
27
27
  self.source = ei_cmd.egg_info
28
28
  self.target = os.path.join(self.install_dir, basename)
29
- self.outputs = []
29
+ self.outputs: list[str] = []
30
30
 
31
31
  def run(self) -> None:
32
32
  self.run_command('egg_info')
@@ -6,9 +6,9 @@ class saveopts(option_base):
6
6
 
7
7
  description = "save supplied options to setup.cfg or other config file"
8
8
 
9
- def run(self):
9
+ def run(self) -> None:
10
10
  dist = self.distribution
11
- settings = {}
11
+ settings: dict[str, dict[str, str]] = {}
12
12
 
13
13
  for cmd in dist.command_options:
14
14
  if cmd == 'saveopts':
@@ -202,10 +202,10 @@ class sdist(orig.sdist):
202
202
  """
203
203
  log.info("reading manifest file '%s'", self.manifest)
204
204
  manifest = open(self.manifest, 'rb')
205
- for line in manifest:
205
+ for bytes_line in manifest:
206
206
  # The manifest must contain UTF-8. See #303.
207
207
  try:
208
- line = line.decode('UTF-8')
208
+ line = bytes_line.decode('UTF-8')
209
209
  except UnicodeDecodeError:
210
210
  log.warn("%r not UTF-8 decodable -- skipping" % line)
211
211
  continue
@@ -37,7 +37,7 @@ def edit_config(filename, settings, dry_run=False):
37
37
  """
38
38
  log.debug("Reading configuration from %s", filename)
39
39
  opts = configparser.RawConfigParser()
40
- opts.optionxform = lambda x: x
40
+ opts.optionxform = lambda optionstr: optionstr # type: ignore[method-assign] # overriding method
41
41
  _cfg_read_utf8_with_fallback(opts, filename)
42
42
 
43
43
  for section, options in settings.items():
@@ -41,19 +41,6 @@ def load_file(filepath: StrPath) -> dict:
41
41
 
42
42
 
43
43
  def validate(config: dict, filepath: StrPath) -> bool:
44
- skip = os.getenv("SETUPTOOLS_DANGEROUSLY_SKIP_PYPROJECT_VALIDATION", "false")
45
- if skip.lower() == "true": # https://github.com/pypa/setuptools/issues/4459
46
- SetuptoolsWarning.emit(
47
- "Skipping the validation of `pyproject.toml`.",
48
- """
49
- Please note that some setuptools functionalities rely on the validation of
50
- `pyproject.toml` against misconfiguration to ensure proper operation.
51
- By skipping the automatic checks, you taking responsibility for making sure
52
- the file is valid. Otherwise unexpected behaviours may occur.
53
- """,
54
- )
55
- return True
56
-
57
44
  from . import _validate_pyproject as validator
58
45
 
59
46
  trove_classifier = validator.FORMAT_FUNCTIONS.get("trove-classifier")
@@ -904,7 +904,7 @@ class Distribution(_Distribution):
904
904
 
905
905
  return nargs
906
906
 
907
- def get_cmdline_options(self):
907
+ def get_cmdline_options(self) -> dict[str, dict[str, str | None]]:
908
908
  """Return a '{cmd: {opt:val}}' map of all command-line options
909
909
 
910
910
  Option names are all long, but do not include the leading '--', and
@@ -914,9 +914,10 @@ class Distribution(_Distribution):
914
914
  Note that options provided by config files are intentionally excluded.
915
915
  """
916
916
 
917
- d = {}
917
+ d: dict[str, dict[str, str | None]] = {}
918
918
 
919
919
  for cmd, opts in self.command_options.items():
920
+ val: str | None
920
921
  for opt, (src, val) in opts.items():
921
922
  if src != "command line":
922
923
  continue
@@ -73,7 +73,7 @@ def patch_all():
73
73
  import setuptools
74
74
 
75
75
  # we can't patch distutils.cmd, alas
76
- distutils.core.Command = setuptools.Command
76
+ distutils.core.Command = setuptools.Command # type: ignore[misc,assignment] # monkeypatching
77
77
 
78
78
  _patch_distribution_metadata()
79
79
 
@@ -82,8 +82,8 @@ def patch_all():
82
82
  module.Distribution = setuptools.dist.Distribution
83
83
 
84
84
  # Install the patched Extension
85
- distutils.core.Extension = setuptools.extension.Extension
86
- distutils.extension.Extension = setuptools.extension.Extension
85
+ distutils.core.Extension = setuptools.extension.Extension # type: ignore[misc,assignment] # monkeypatching
86
+ distutils.extension.Extension = setuptools.extension.Extension # type: ignore[misc,assignment] # monkeypatching
87
87
  if 'distutils.command.build_ext' in sys.modules:
88
88
  sys.modules[
89
89
  'distutils.command.build_ext'
@@ -20,7 +20,7 @@ from more_itertools import unique_everseen
20
20
  import distutils.errors
21
21
 
22
22
  if TYPE_CHECKING:
23
- from typing_extensions import NotRequired
23
+ from typing_extensions import LiteralString, NotRequired
24
24
 
25
25
  # https://github.com/python/mypy/issues/8166
26
26
  if not TYPE_CHECKING and platform.system() == 'Windows':
@@ -426,7 +426,7 @@ class SystemInfo:
426
426
  vs_vers.append(ver)
427
427
  return sorted(vs_vers)
428
428
 
429
- def find_programdata_vs_vers(self):
429
+ def find_programdata_vs_vers(self) -> dict[float, str]:
430
430
  r"""
431
431
  Find Visual studio 2017+ versions from information in
432
432
  "C:\ProgramData\Microsoft\VisualStudio\Packages\_Instances".
@@ -436,7 +436,7 @@ class SystemInfo:
436
436
  dict
437
437
  float version as key, path as value.
438
438
  """
439
- vs_versions = {}
439
+ vs_versions: dict[float, str] = {}
440
440
  instances_dir = r'C:\ProgramData\Microsoft\VisualStudio\Packages\_Instances'
441
441
 
442
442
  try:
@@ -573,7 +573,7 @@ class SystemInfo:
573
573
  return self.ri.lookup(self.ri.vc, '%0.1f' % self.vs_ver) or default_vc
574
574
 
575
575
  @property
576
- def WindowsSdkVersion(self):
576
+ def WindowsSdkVersion(self) -> tuple[LiteralString, ...]:
577
577
  """
578
578
  Microsoft Windows SDK versions for specified MSVC++ version.
579
579
 
@@ -592,7 +592,7 @@ class SystemInfo:
592
592
  return '8.1', '8.1a'
593
593
  elif self.vs_ver >= 14.0:
594
594
  return '10.0', '8.1'
595
- return None
595
+ return ()
596
596
 
597
597
  @property
598
598
  def WindowsSdkLastVersion(self):
@@ -607,7 +607,7 @@ class SystemInfo:
607
607
  return self._use_last_dir_name(os.path.join(self.WindowsSdkDir, 'lib'))
608
608
 
609
609
  @property
610
- def WindowsSdkDir(self): # noqa: C901 # is too complex (12) # FIXME
610
+ def WindowsSdkDir(self) -> str | None: # noqa: C901 # is too complex (12) # FIXME
611
611
  """
612
612
  Microsoft Windows SDK directory.
613
613
 
@@ -616,7 +616,7 @@ class SystemInfo:
616
616
  str
617
617
  path
618
618
  """
619
- sdkdir = ''
619
+ sdkdir: str | None = ''
620
620
  for ver in self.WindowsSdkVersion:
621
621
  # Try to get it from registry
622
622
  loc = os.path.join(self.ri.windows_sdk, 'v%s' % ver)
@@ -800,7 +800,7 @@ class SystemInfo:
800
800
  return self.ri.lookup(self.ri.vc, 'frameworkdir64') or guess_fw
801
801
 
802
802
  @property
803
- def FrameworkVersion32(self):
803
+ def FrameworkVersion32(self) -> tuple[str, ...]:
804
804
  """
805
805
  Microsoft .NET Framework 32bit versions.
806
806
 
@@ -812,7 +812,7 @@ class SystemInfo:
812
812
  return self._find_dot_net_versions(32)
813
813
 
814
814
  @property
815
- def FrameworkVersion64(self):
815
+ def FrameworkVersion64(self) -> tuple[str, ...]:
816
816
  """
817
817
  Microsoft .NET Framework 64bit versions.
818
818
 
@@ -823,7 +823,7 @@ class SystemInfo:
823
823
  """
824
824
  return self._find_dot_net_versions(64)
825
825
 
826
- def _find_dot_net_versions(self, bits):
826
+ def _find_dot_net_versions(self, bits) -> tuple[str, ...]:
827
827
  """
828
828
  Find Microsoft .NET Framework versions.
829
829
 
@@ -851,7 +851,7 @@ class SystemInfo:
851
851
  return 'v3.5', 'v2.0.50727'
852
852
  elif self.vs_ver == 8.0:
853
853
  return 'v3.0', 'v2.0.50727'
854
- return None
854
+ return ()
855
855
 
856
856
  @staticmethod
857
857
  def _use_last_dir_name(path, prefix=''):
@@ -17,7 +17,6 @@ from setuptools.config.pyprojecttoml import (
17
17
  )
18
18
  from setuptools.dist import Distribution
19
19
  from setuptools.errors import OptionError
20
- from setuptools.warnings import SetuptoolsWarning
21
20
 
22
21
  import distutils.core
23
22
 
@@ -395,9 +394,3 @@ def test_warn_tools_typo(tmp_path):
395
394
 
396
395
  with pytest.warns(_ToolsTypoInMetadata):
397
396
  read_configuration(pyproject)
398
-
399
-
400
- def test_warn_skipping_validation(monkeypatch):
401
- monkeypatch.setenv("SETUPTOOLS_DANGEROUSLY_SKIP_PYPROJECT_VALIDATION", "true")
402
- with pytest.warns(SetuptoolsWarning, match="Skipping the validation"):
403
- assert validate({"completely-wrong": "data"}, "pyproject.toml") is True
@@ -1,16 +1,28 @@
1
+ from __future__ import annotations
2
+
1
3
  import functools
2
4
  import importlib
3
5
  import io
4
6
  from email import message_from_string
7
+ from email.generator import Generator
8
+ from email.message import Message
9
+ from email.parser import Parser
10
+ from email.policy import EmailPolicy
11
+ from pathlib import Path
12
+ from unittest.mock import Mock
5
13
 
6
14
  import pytest
7
15
  from packaging.metadata import Metadata
16
+ from packaging.requirements import Requirement
8
17
 
9
18
  from setuptools import _reqs, sic
10
19
  from setuptools._core_metadata import rfc822_escape, rfc822_unescape
11
20
  from setuptools.command.egg_info import egg_info, write_requirements
21
+ from setuptools.config import expand, setupcfg
12
22
  from setuptools.dist import Distribution
13
23
 
24
+ from .config.downloads import retrieve_file, urls_from_file
25
+
14
26
  EXAMPLE_BASE_INFO = dict(
15
27
  name="package",
16
28
  version="0.0.1",
@@ -303,84 +315,168 @@ def test_maintainer_author(name, attrs, tmpdir):
303
315
  assert line in pkg_lines_set
304
316
 
305
317
 
306
- def test_parity_with_metadata_from_pypa_wheel(tmp_path):
307
- attrs = dict(
308
- **EXAMPLE_BASE_INFO,
309
- # Example with complex requirement definition
310
- python_requires=">=3.8",
311
- install_requires="""
312
- packaging==23.2
313
- more-itertools==8.8.0; extra == "other"
314
- jaraco.text==3.7.0
315
- importlib-resources==5.10.2; python_version<"3.8"
316
- importlib-metadata==6.0.0 ; python_version<"3.8"
317
- colorama>=0.4.4; sys_platform == "win32"
318
- """,
319
- extras_require={
320
- "testing": """
321
- pytest >= 6
322
- pytest-checkdocs >= 2.4
323
- tomli ; \\
324
- # Using stdlib when possible
325
- python_version < "3.11"
326
- ini2toml[lite]>=0.9
327
- """,
328
- "other": [],
329
- },
318
+ class TestParityWithMetadataFromPyPaWheel:
319
+ def base_example(self):
320
+ attrs = dict(
321
+ **EXAMPLE_BASE_INFO,
322
+ # Example with complex requirement definition
323
+ python_requires=">=3.8",
324
+ install_requires="""
325
+ packaging==23.2
326
+ more-itertools==8.8.0; extra == "other"
327
+ jaraco.text==3.7.0
328
+ importlib-resources==5.10.2; python_version<"3.8"
329
+ importlib-metadata==6.0.0 ; python_version<"3.8"
330
+ colorama>=0.4.4; sys_platform == "win32"
331
+ """,
332
+ extras_require={
333
+ "testing": """
334
+ pytest >= 6
335
+ pytest-checkdocs >= 2.4
336
+ tomli ; \\
337
+ # Using stdlib when possible
338
+ python_version < "3.11"
339
+ ini2toml[lite]>=0.9
340
+ """,
341
+ "other": [],
342
+ },
343
+ )
344
+ # Generate a PKG-INFO file using setuptools
345
+ return Distribution(attrs)
346
+
347
+ def test_requires_dist(self, tmp_path):
348
+ dist = self.base_example()
349
+ pkg_info = _get_pkginfo(dist)
350
+ assert _valid_metadata(pkg_info)
351
+
352
+ # Ensure Requires-Dist is present
353
+ expected = [
354
+ 'Metadata-Version:',
355
+ 'Requires-Python: >=3.8',
356
+ 'Provides-Extra: other',
357
+ 'Provides-Extra: testing',
358
+ 'Requires-Dist: tomli; python_version < "3.11" and extra == "testing"',
359
+ 'Requires-Dist: more-itertools==8.8.0; extra == "other"',
360
+ 'Requires-Dist: ini2toml[lite]>=0.9; extra == "testing"',
361
+ ]
362
+ for line in expected:
363
+ assert line in pkg_info
364
+
365
+ HERE = Path(__file__).parent
366
+ EXAMPLES_FILE = HERE / "config/setupcfg_examples.txt"
367
+
368
+ @pytest.fixture(params=[None, *urls_from_file(EXAMPLES_FILE)])
369
+ def dist(self, request, monkeypatch, tmp_path):
370
+ """Example of distribution with arbitrary configuration"""
371
+ monkeypatch.chdir(tmp_path)
372
+ monkeypatch.setattr(expand, "read_attr", Mock(return_value="0.42"))
373
+ monkeypatch.setattr(expand, "read_files", Mock(return_value="hello world"))
374
+ if request.param is None:
375
+ yield self.base_example()
376
+ else:
377
+ # Real-world usage
378
+ config = retrieve_file(request.param)
379
+ yield setupcfg.apply_configuration(Distribution({}), config)
380
+
381
+ @pytest.mark.uses_network
382
+ def test_equivalent_output(self, tmp_path, dist):
383
+ """Ensure output from setuptools is equivalent to the one from `pypa/wheel`"""
384
+ # Generate a METADATA file using pypa/wheel for comparison
385
+ wheel_metadata = importlib.import_module("wheel.metadata")
386
+ pkginfo_to_metadata = getattr(wheel_metadata, "pkginfo_to_metadata", None)
387
+
388
+ if pkginfo_to_metadata is None: # pragma: nocover
389
+ pytest.xfail(
390
+ "wheel.metadata.pkginfo_to_metadata is undefined, "
391
+ "(this is likely to be caused by API changes in pypa/wheel"
392
+ )
393
+
394
+ # Generate an simplified "egg-info" dir for pypa/wheel to convert
395
+ pkg_info = _get_pkginfo(dist)
396
+ egg_info_dir = tmp_path / "pkg.egg-info"
397
+ egg_info_dir.mkdir(parents=True)
398
+ (egg_info_dir / "PKG-INFO").write_text(pkg_info, encoding="utf-8")
399
+ write_requirements(egg_info(dist), egg_info_dir, egg_info_dir / "requires.txt")
400
+
401
+ # Get pypa/wheel generated METADATA but normalize requirements formatting
402
+ metadata_msg = pkginfo_to_metadata(egg_info_dir, egg_info_dir / "PKG-INFO")
403
+ metadata_str = _normalize_metadata(metadata_msg)
404
+ pkg_info_msg = message_from_string(pkg_info)
405
+ pkg_info_str = _normalize_metadata(pkg_info_msg)
406
+
407
+ # Compare setuptools PKG-INFO x pypa/wheel METADATA
408
+ assert metadata_str == pkg_info_str
409
+
410
+ # Make sure it parses/serializes well in pypa/wheel
411
+ _assert_roundtrip_message(pkg_info)
412
+
413
+
414
+ def _assert_roundtrip_message(metadata: str) -> None:
415
+ """Emulate the way wheel.bdist_wheel parses and regenerates the message,
416
+ then ensures the metadata generated by setuptools is compatible.
417
+ """
418
+ with io.StringIO(metadata) as buffer:
419
+ msg = Parser().parse(buffer)
420
+
421
+ serialization_policy = EmailPolicy(
422
+ utf8=True,
423
+ mangle_from_=False,
424
+ max_line_length=0,
330
425
  )
331
- # Generate a PKG-INFO file using setuptools
332
- dist = Distribution(attrs)
333
- with io.StringIO() as fp:
334
- dist.metadata.write_pkg_file(fp)
335
- pkg_info = fp.getvalue()
426
+ with io.BytesIO() as buffer:
427
+ out = io.TextIOWrapper(buffer, encoding="utf-8")
428
+ Generator(out, policy=serialization_policy).flatten(msg)
429
+ out.flush()
430
+ regenerated = buffer.getvalue()
431
+
432
+ raw_metadata = bytes(metadata, "utf-8")
433
+ # Normalise newlines to avoid test errors on Windows:
434
+ raw_metadata = b"\n".join(raw_metadata.splitlines())
435
+ regenerated = b"\n".join(regenerated.splitlines())
436
+ assert regenerated == raw_metadata
437
+
438
+
439
+ def _normalize_metadata(msg: Message) -> str:
440
+ """Allow equivalent metadata to be compared directly"""
441
+ # The main challenge regards the requirements and extras.
442
+ # Both setuptools and wheel already apply some level of normalization
443
+ # but they differ regarding which character is chosen, according to the
444
+ # following spec it should be "-":
445
+ # https://packaging.python.org/en/latest/specifications/name-normalization/
446
+
447
+ # Related issues:
448
+ # https://github.com/pypa/packaging/issues/845
449
+ # https://github.com/pypa/packaging/issues/644#issuecomment-2429813968
450
+
451
+ extras = {x.replace("_", "-"): x for x in msg.get_all("Provides-Extra", [])}
452
+ reqs = [
453
+ _normalize_req(req, extras)
454
+ for req in _reqs.parse(msg.get_all("Requires-Dist", []))
455
+ ]
456
+ del msg["Requires-Dist"]
457
+ del msg["Provides-Extra"]
336
458
 
337
- assert _valid_metadata(pkg_info)
459
+ # Ensure consistent ord
460
+ for req in sorted(reqs):
461
+ msg["Requires-Dist"] = req
462
+ for extra in sorted(extras):
463
+ msg["Provides-Extra"] = extra
338
464
 
339
- # Ensure Requires-Dist is present
340
- expected = [
341
- 'Metadata-Version:',
342
- 'Requires-Python: >=3.8',
343
- 'Provides-Extra: other',
344
- 'Provides-Extra: testing',
345
- 'Requires-Dist: tomli; python_version < "3.11" and extra == "testing"',
346
- 'Requires-Dist: more-itertools==8.8.0; extra == "other"',
347
- 'Requires-Dist: ini2toml[lite]>=0.9; extra == "testing"',
348
- ]
349
- for line in expected:
350
- assert line in pkg_info
465
+ return msg.as_string()
351
466
 
352
- # Generate a METADATA file using pypa/wheel for comparison
353
- wheel_metadata = importlib.import_module("wheel.metadata")
354
- pkginfo_to_metadata = getattr(wheel_metadata, "pkginfo_to_metadata", None)
355
467
 
356
- if pkginfo_to_metadata is None:
357
- pytest.xfail(
358
- "wheel.metadata.pkginfo_to_metadata is undefined, "
359
- "(this is likely to be caused by API changes in pypa/wheel"
360
- )
468
+ def _normalize_req(req: Requirement, extras: dict[str, str]) -> str:
469
+ """Allow equivalent requirement objects to be compared directly"""
470
+ as_str = str(req).replace(req.name, req.name.replace("_", "-"))
471
+ for norm, orig in extras.items():
472
+ as_str = as_str.replace(orig, norm)
473
+ return as_str
361
474
 
362
- # Generate an simplified "egg-info" dir for pypa/wheel to convert
363
- egg_info_dir = tmp_path / "pkg.egg-info"
364
- egg_info_dir.mkdir(parents=True)
365
- (egg_info_dir / "PKG-INFO").write_text(pkg_info, encoding="utf-8")
366
- write_requirements(egg_info(dist), egg_info_dir, egg_info_dir / "requires.txt")
367
-
368
- # Get pypa/wheel generated METADATA but normalize requirements formatting
369
- metadata_msg = pkginfo_to_metadata(egg_info_dir, egg_info_dir / "PKG-INFO")
370
- metadata_deps = set(_reqs.parse(metadata_msg.get_all("Requires-Dist")))
371
- metadata_extras = set(metadata_msg.get_all("Provides-Extra"))
372
- del metadata_msg["Requires-Dist"]
373
- del metadata_msg["Provides-Extra"]
374
- pkg_info_msg = message_from_string(pkg_info)
375
- pkg_info_deps = set(_reqs.parse(pkg_info_msg.get_all("Requires-Dist")))
376
- pkg_info_extras = set(pkg_info_msg.get_all("Provides-Extra"))
377
- del pkg_info_msg["Requires-Dist"]
378
- del pkg_info_msg["Provides-Extra"]
379
-
380
- # Compare setuptools PKG-INFO x pypa/wheel METADATA
381
- assert metadata_msg.as_string() == pkg_info_msg.as_string()
382
- assert metadata_deps == pkg_info_deps
383
- assert metadata_extras == pkg_info_extras
475
+
476
+ def _get_pkginfo(dist: Distribution):
477
+ with io.StringIO() as fp:
478
+ dist.metadata.write_pkg_file(fp)
479
+ return fp.getvalue()
384
480
 
385
481
 
386
482
  def _valid_metadata(text: str) -> bool:
@@ -1,6 +1,6 @@
1
1
  import sys
2
2
  import unicodedata
3
- from configparser import ConfigParser
3
+ from configparser import RawConfigParser
4
4
 
5
5
  from .compat import py39
6
6
  from .warnings import SetuptoolsDeprecationWarning
@@ -65,10 +65,10 @@ def _read_utf8_with_fallback(file: str, fallback_encoding=py39.LOCALE_ENCODING)
65
65
 
66
66
 
67
67
  def _cfg_read_utf8_with_fallback(
68
- cfg: ConfigParser, file: str, fallback_encoding=py39.LOCALE_ENCODING
68
+ cfg: RawConfigParser, file: str, fallback_encoding=py39.LOCALE_ENCODING
69
69
  ) -> None:
70
70
  """Same idea as :func:`_read_utf8_with_fallback`, but for the
71
- :meth:`ConfigParser.read` method.
71
+ :meth:`RawConfigParser.read` method.
72
72
 
73
73
  This method may call ``cfg.clear()``.
74
74
  """
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.45.0"
3
+ __version__ = "0.45.1"