req-compile 1.1.2__tar.gz → 1.2.0__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 (61) hide show
  1. {req_compile-1.1.2 → req_compile-1.2.0}/CHANGELOG.rst +13 -0
  2. {req_compile-1.1.2/req_compile.egg-info → req_compile-1.2.0}/PKG-INFO +44 -1
  3. {req_compile-1.1.2 → req_compile-1.2.0}/README.rst +30 -0
  4. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile/candidates.py +18 -3
  5. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile/cmdline.py +154 -10
  6. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile/compile.py +7 -1
  7. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile/containers.py +38 -9
  8. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile/dists.py +55 -15
  9. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile/metadata/extractor.py +6 -1
  10. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile/repos/findlinks.py +3 -1
  11. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile/repos/multi.py +14 -3
  12. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile/repos/pypi.py +38 -26
  13. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile/repos/repository.py +102 -170
  14. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile/repos/solution.py +154 -8
  15. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile/repos/source.py +6 -1
  16. req_compile-1.2.0/req_compile/target.py +561 -0
  17. {req_compile-1.1.2 → req_compile-1.2.0/req_compile.egg-info}/PKG-INFO +44 -1
  18. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile.egg-info/SOURCES.txt +4 -0
  19. req_compile-1.2.0/tests/test_cross_platform.py +47 -0
  20. {req_compile-1.1.2 → req_compile-1.2.0}/tests/test_extractor.py +22 -0
  21. {req_compile-1.1.2 → req_compile-1.2.0}/tests/test_repositories.py +6 -14
  22. req_compile-1.2.0/tests/test_solution_target.py +182 -0
  23. req_compile-1.2.0/tests/test_target.py +406 -0
  24. {req_compile-1.1.2 → req_compile-1.2.0}/version.bzl +1 -1
  25. {req_compile-1.1.2 → req_compile-1.2.0}/LICENSE.txt +0 -0
  26. {req_compile-1.1.2 → req_compile-1.2.0}/MANIFEST.in +0 -0
  27. {req_compile-1.1.2 → req_compile-1.2.0}/pyproject.toml +0 -0
  28. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile/__init__.py +0 -0
  29. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile/__main__.py +0 -0
  30. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile/config.py +0 -0
  31. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile/errors.py +0 -0
  32. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile/filename.py +0 -0
  33. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile/metadata/__init__.py +0 -0
  34. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile/metadata/dist_info.py +0 -0
  35. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile/metadata/metadata.py +0 -0
  36. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile/metadata/patch.py +0 -0
  37. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile/metadata/pyproject.py +0 -0
  38. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile/metadata/source.py +0 -0
  39. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile/py.typed +0 -0
  40. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile/repos/__init__.py +0 -0
  41. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile/utils.py +0 -0
  42. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile/versions.py +0 -0
  43. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile.egg-info/dependency_links.txt +0 -0
  44. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile.egg-info/entry_points.txt +0 -0
  45. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile.egg-info/requires.txt +0 -0
  46. {req_compile-1.1.2 → req_compile-1.2.0}/req_compile.egg-info/top_level.txt +0 -0
  47. {req_compile-1.1.2 → req_compile-1.2.0}/requirements.in +0 -0
  48. {req_compile-1.1.2 → req_compile-1.2.0}/setup.cfg +0 -0
  49. {req_compile-1.1.2 → req_compile-1.2.0}/setup.py +0 -0
  50. {req_compile-1.1.2 → req_compile-1.2.0}/tests/test_cmdline.py +0 -0
  51. {req_compile-1.1.2 → req_compile-1.2.0}/tests/test_compile.py +0 -0
  52. {req_compile-1.1.2 → req_compile-1.2.0}/tests/test_containers.py +0 -0
  53. {req_compile-1.1.2 → req_compile-1.2.0}/tests/test_dists.py +0 -0
  54. {req_compile-1.1.2 → req_compile-1.2.0}/tests/test_egg_info.py +0 -0
  55. {req_compile-1.1.2 → req_compile-1.2.0}/tests/test_filename.py +0 -0
  56. {req_compile-1.1.2 → req_compile-1.2.0}/tests/test_integration.py +0 -0
  57. {req_compile-1.1.2 → req_compile-1.2.0}/tests/test_merge_requirements.py +0 -0
  58. {req_compile-1.1.2 → req_compile-1.2.0}/tests/test_metadata.py +0 -0
  59. {req_compile-1.1.2 → req_compile-1.2.0}/tests/test_no_setup_py.py +0 -0
  60. {req_compile-1.1.2 → req_compile-1.2.0}/tests/test_utils.py +0 -0
  61. {req_compile-1.1.2 → req_compile-1.2.0}/tests/test_versions.py +0 -0
@@ -1,3 +1,16 @@
1
+ New in version 1.2.0:
2
+
3
+ * Cross-platform compilation. ``--platform``, ``--python-version`` and ``--implementation`` select
4
+ wheels and evaluate environment markers for an environment other than the one running ``req-compile``.
5
+ ``--platform`` takes a wheel platform tag such as ``manylinux_2_28_x86_64`` or ``macosx_11_0_arm64``,
6
+ which is recorded in the header of lock files compiled by Bazel.
7
+ * The ``py_reqs_compiler`` Bazel rule has new ``platform`` and ``python_version`` attributes, so the
8
+ solutions for every platform a project supports can be compiled and tested from a single machine.
9
+ Solutions record their platform and Python version in their header, and a solution compiled for
10
+ another target is not recycled when recompiling without ``--upgrade``.
11
+ * Removed the ``py_reqs_remote_compiler`` Bazel rule. Use the ``platform`` attribute of ``py_reqs_compiler``
12
+ instead.
13
+
1
14
  New in version 1.1.0:
2
15
 
3
16
  * Python 3.13 and 3.14 support.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: req-compile
3
- Version: 1.1.2
3
+ Version: 1.2.0
4
4
  Summary: Python requirements compiler
5
5
  Home-page: https://github.com/periareon/req-compile
6
6
  Author: Spencer Putt
@@ -38,6 +38,19 @@ Dynamic: requires-dist
38
38
  Dynamic: requires-python
39
39
  Dynamic: summary
40
40
 
41
+ New in version 1.2.0:
42
+
43
+ * Cross-platform compilation. ``--platform``, ``--python-version`` and ``--implementation`` select
44
+ wheels and evaluate environment markers for an environment other than the one running ``req-compile``.
45
+ ``--platform`` takes a wheel platform tag such as ``manylinux_2_28_x86_64`` or ``macosx_11_0_arm64``,
46
+ which is recorded in the header of lock files compiled by Bazel.
47
+ * The ``py_reqs_compiler`` Bazel rule has new ``platform`` and ``python_version`` attributes, so the
48
+ solutions for every platform a project supports can be compiled and tested from a single machine.
49
+ Solutions record their platform and Python version in their header, and a solution compiled for
50
+ another target is not recycled when recompiling without ``--upgrade``.
51
+ * Removed the ``py_reqs_remote_compiler`` Bazel rule. Use the ``platform`` attribute of ``py_reqs_compiler``
52
+ instead.
53
+
41
54
  New in version 1.1.0:
42
55
 
43
56
  * Python 3.13 and 3.14 support.
@@ -112,6 +125,9 @@ To get started with Bazel, add the following to your ``MODULE.bazel`` file:
112
125
  )
113
126
  use_repo(requirements, "my_pip_deps")
114
127
 
128
+ The solutions for every platform can be compiled from a single machine with the ``platform`` attribute of
129
+ ``py_reqs_compiler``. See the `Bazel rules documentation <docs/docs.md>`_ for details.
130
+
115
131
 
116
132
  Why use it?
117
133
  -----------
@@ -229,6 +245,33 @@ was passed on the commandline.
229
245
  By default, PyPI (https://pypi.org/) or the default pip index is added as a default repository. It can be removed by passing
230
246
  ``--no-index`` on the commandline or passing a different index via ``--index-url``.
231
247
 
248
+ Compiling for another platform
249
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
250
+ By default a solution is compiled for the interpreter running Req-Compile: wheels are selected and
251
+ environment markers are evaluated for its platform, Python version and implementation. A solution for
252
+ a different environment can be compiled from any machine with the target options::
253
+
254
+ > req-compile requirements.txt --platform macosx_11_0_arm64 --python-version 3.11 --only-binary :all:
255
+
256
+ * ``--platform``
257
+
258
+ The platform to compile for as a wheel platform tag, the same values ``pip --platform`` accepts:
259
+ ``manylinux_2_28_x86_64``, ``manylinux2014_aarch64``, ``macosx_11_0_arm64``, ``macosx_10_15_x86_64``,
260
+ ``win_amd64`` and so on. A manylinux tag accepts wheels built for that glibc or older, a macosx tag
261
+ wheels built for that release or older, and plain ``linux_x86_64`` accepts no manylinux wheels at all,
262
+ as with pip.
263
+ * ``--python-version``
264
+
265
+ The Python version to compile for, e.g. ``3.11`` or ``3.11.4``. This can be used on its own to
266
+ compile for a different version of Python on the current platform.
267
+ * ``--implementation``
268
+
269
+ The interpreter tag of the implementation to compile for, ``cp`` for CPython or ``pp`` for PyPy.
270
+
271
+ Source distributions have their metadata extracted by running them on the machine performing the
272
+ compilation, which may not match the target. Prefer ``--only-binary :all:`` when compiling for another
273
+ platform. ``req-candidates`` accepts the same options to show the candidates available for a target.
274
+
232
275
  Identifying source of constraints
233
276
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
234
277
  Why did I just get version 1.11.0 of ``six``? Find out by examining the output::
@@ -47,6 +47,9 @@ To get started with Bazel, add the following to your ``MODULE.bazel`` file:
47
47
  )
48
48
  use_repo(requirements, "my_pip_deps")
49
49
 
50
+ The solutions for every platform can be compiled from a single machine with the ``platform`` attribute of
51
+ ``py_reqs_compiler``. See the `Bazel rules documentation <docs/docs.md>`_ for details.
52
+
50
53
 
51
54
  Why use it?
52
55
  -----------
@@ -164,6 +167,33 @@ was passed on the commandline.
164
167
  By default, PyPI (https://pypi.org/) or the default pip index is added as a default repository. It can be removed by passing
165
168
  ``--no-index`` on the commandline or passing a different index via ``--index-url``.
166
169
 
170
+ Compiling for another platform
171
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
172
+ By default a solution is compiled for the interpreter running Req-Compile: wheels are selected and
173
+ environment markers are evaluated for its platform, Python version and implementation. A solution for
174
+ a different environment can be compiled from any machine with the target options::
175
+
176
+ > req-compile requirements.txt --platform macosx_11_0_arm64 --python-version 3.11 --only-binary :all:
177
+
178
+ * ``--platform``
179
+
180
+ The platform to compile for as a wheel platform tag, the same values ``pip --platform`` accepts:
181
+ ``manylinux_2_28_x86_64``, ``manylinux2014_aarch64``, ``macosx_11_0_arm64``, ``macosx_10_15_x86_64``,
182
+ ``win_amd64`` and so on. A manylinux tag accepts wheels built for that glibc or older, a macosx tag
183
+ wheels built for that release or older, and plain ``linux_x86_64`` accepts no manylinux wheels at all,
184
+ as with pip.
185
+ * ``--python-version``
186
+
187
+ The Python version to compile for, e.g. ``3.11`` or ``3.11.4``. This can be used on its own to
188
+ compile for a different version of Python on the current platform.
189
+ * ``--implementation``
190
+
191
+ The interpreter tag of the implementation to compile for, ``cp`` for CPython or ``pp`` for PyPy.
192
+
193
+ Source distributions have their metadata extracted by running them on the machine performing the
194
+ compilation, which may not match the target. Prefer ``--only-binary :all:`` when compiling for another
195
+ platform. ``req-candidates`` accepts the same options to show the candidates available for a target.
196
+
167
197
  Identifying source of constraints
168
198
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
169
199
  Why did I just get version 1.11.0 of ``six``? Find out by examining the output::
@@ -14,7 +14,9 @@ from req_compile.cmdline import (
14
14
  SplitProjectsFilter,
15
15
  add_logging_args,
16
16
  add_repo_args,
17
+ add_target_args,
17
18
  build_repo,
19
+ build_target,
18
20
  )
19
21
  from req_compile.repos.pypi import PyPIRepository
20
22
  from req_compile.repos.repository import (
@@ -71,6 +73,7 @@ def candidates_main() -> None:
71
73
  )
72
74
  add_logging_args(parser)
73
75
  add_repo_args(parser)
76
+ add_target_args(parser)
74
77
  args = parser.parse_args()
75
78
 
76
79
  logger = logging.getLogger("req_compile")
@@ -81,6 +84,12 @@ def candidates_main() -> None:
81
84
  logging.basicConfig(level=logging.CRITICAL, stream=sys.stderr)
82
85
  logging.getLogger().setLevel(level=logging.CRITICAL)
83
86
 
87
+ try:
88
+ target = build_target(args)
89
+ except ValueError as ex:
90
+ print(f"ERROR: {ex}", file=sys.stderr)
91
+ sys.exit(1)
92
+
84
93
  start = time.monotonic()
85
94
  wheeldir = tempfile.mkdtemp(suffix="-wheeldir")
86
95
  repo = build_repo(
@@ -94,10 +103,13 @@ def candidates_main() -> None:
94
103
  no_index=args.no_index,
95
104
  wheeldir=wheeldir,
96
105
  allow_prerelease=args.allow_prerelease,
106
+ target=target,
97
107
  )
98
108
 
99
109
  if isinstance(repo, PyPIRepository) and args.project_name is None:
100
- repo = SourceRepository(".", excluded_paths=args.excluded_sources)
110
+ repo = SourceRepository(
111
+ ".", excluded_paths=args.excluded_sources, target=target
112
+ )
101
113
 
102
114
  total_candidates = 0
103
115
  try:
@@ -112,10 +124,13 @@ def candidates_main() -> None:
112
124
  candidates = repo.get_candidates(req)
113
125
  if not args.all:
114
126
  candidates = filter_candidates(
115
- req, candidates, allow_prereleases=args.allow_prerelease
127
+ req,
128
+ candidates,
129
+ allow_prereleases=args.allow_prerelease,
130
+ target=repo.target,
116
131
  )
117
132
 
118
- for candidate in sort_candidates(candidates):
133
+ for candidate in sort_candidates(candidates, target=repo.target):
119
134
  if (
120
135
  candidate.type == DistributionType.SDIST
121
136
  and candidate.name in args.only_binary
@@ -45,6 +45,7 @@ from req_compile.repos.solution import (
45
45
  SolutionRepository,
46
46
  )
47
47
  from req_compile.repos.source import SourceRepository
48
+ from req_compile.target import TargetEnvironment, resolve
48
49
  from req_compile.utils import (
49
50
  NormName,
50
51
  normalize_project_name,
@@ -56,6 +57,8 @@ from req_compile.versions import is_possible
56
57
  # Blacklist of requirements that will be filtered out of the output
57
58
  BLACKLIST: Iterable[str] = []
58
59
 
60
+ LOG = logging.getLogger("req_compile.cmdline")
61
+
59
62
 
60
63
  def _cantusereason_to_text(
61
64
  reason: CantUseReason,
@@ -147,7 +150,7 @@ def _generate_no_candidate_display(
147
150
  pinned_req = parse_requirement(f"{walkback_project}=={version}")
148
151
  print(f" {pinned_req}", file=sys.stderr, end="")
149
152
  try:
150
- collection = DistributionCollection()
153
+ collection = DistributionCollection(target=repo.target)
151
154
  collection.add_dist(
152
155
  repo.get_dist(pinned_req)[0], None, pinned_req
153
156
  )
@@ -263,7 +266,9 @@ def _dump_repo_candidates(
263
266
  print(" {}:".format(repo), file=sys.stderr)
264
267
  if candidates:
265
268
  attempted_versions = set()
266
- for num, candidate in enumerate(sort_candidates(candidates)):
269
+ for num, candidate in enumerate(
270
+ sort_candidates(candidates, target=repo.target)
271
+ ):
267
272
  if not req.specifier.contains(candidate.version):
268
273
  continue
269
274
  attempted_versions.add(candidate.version)
@@ -682,7 +687,13 @@ def build_repo(
682
687
  extra_index_urls: Optional[Iterable[str]] = None,
683
688
  no_index: bool = False,
684
689
  allow_prerelease: bool = False,
690
+ target: Optional[TargetEnvironment] = None,
685
691
  ) -> Repository:
692
+ """Build the repository to compile against from command line style options.
693
+
694
+ Args:
695
+ target: The environment to compile for. Defaults to the host.
696
+ """
686
697
  pooled_repos: List[Repository] = []
687
698
  if find_links:
688
699
  pooled_repos.extend(
@@ -692,6 +703,7 @@ def build_repo(
692
703
  relative_to=(
693
704
  find_links[find_link] if isinstance(find_links, Mapping) else None
694
705
  ),
706
+ target=target,
695
707
  )
696
708
  for find_link in find_links
697
709
  )
@@ -704,6 +716,7 @@ def build_repo(
704
716
  wheeldir,
705
717
  allow_prerelease=allow_prerelease,
706
718
  index_type=IndexType.DEFAULT,
719
+ target=target,
707
720
  )
708
721
  )
709
722
  else:
@@ -713,6 +726,7 @@ def build_repo(
713
726
  wheeldir,
714
727
  allow_prerelease=allow_prerelease,
715
728
  index_type=IndexType.INDEX_URL,
729
+ target=target,
716
730
  )
717
731
  for index_url in index_urls
718
732
  )
@@ -723,38 +737,93 @@ def build_repo(
723
737
  wheeldir,
724
738
  allow_prerelease=allow_prerelease,
725
739
  index_type=IndexType.EXTRA_INDEX_URL,
740
+ target=target,
726
741
  )
727
742
  for index_url in extra_index_urls
728
743
  )
729
744
 
730
745
  repos: List[Repository] = []
731
- if solutions:
732
- repos.extend(
733
- SolutionRepository(solution, excluded_packages=upgrade_packages)
734
- for solution in solutions
735
- )
746
+ ignored_solutions: List[SolutionRepository] = []
747
+ for solution_repo in load_solutions(solutions, upgrade_packages, target):
748
+ if solution_repo.matches_target(target):
749
+ repos.append(solution_repo)
750
+ else:
751
+ ignored_solutions.append(solution_repo)
736
752
  if sources:
737
753
  repos.extend(
738
- SourceRepository(source, excluded_paths=excluded_sources)
754
+ SourceRepository(source, excluded_paths=excluded_sources, target=target)
739
755
  for source in sources
740
756
  )
741
757
 
742
758
  if len(pooled_repos) > 1:
743
- repos.append(PooledCandidateMultiRepository(*pooled_repos))
759
+ repos.append(PooledCandidateMultiRepository(*pooled_repos, target=target))
744
760
  elif len(pooled_repos) == 1:
745
761
  repos.append(pooled_repos[0])
746
762
 
747
763
  if not repos:
764
+ if ignored_solutions:
765
+ raise RepositoryInitializationError(
766
+ SolutionRepository,
767
+ "The solution {} was compiled for {}, not for {}. Recompile it for the new target.".format(
768
+ ", ".join(repr(repo) for repo in ignored_solutions),
769
+ _recorded_target(ignored_solutions[0]),
770
+ resolve(target),
771
+ ),
772
+ )
748
773
  raise ValueError(
749
774
  "At least one source of Python distributions must be provided."
750
775
  )
751
776
  if len(repos) > 1:
752
- repo: Repository = MultiRepository(*repos)
777
+ repo: Repository = MultiRepository(*repos, target=target)
753
778
  else:
754
779
  repo = repos[0]
755
780
  return repo
756
781
 
757
782
 
783
+ def _recorded_target(solution: SolutionRepository) -> str:
784
+ return "{} ({})".format(
785
+ solution.recorded_platform or "an unknown platform",
786
+ solution.recorded_python or "unknown Python version",
787
+ )
788
+
789
+
790
+ def load_solutions(
791
+ solutions: Iterable[str],
792
+ upgrade_packages: Iterable[str],
793
+ target: Optional[TargetEnvironment] = None,
794
+ ) -> List[SolutionRepository]:
795
+ """Load previous solutions, warning about any compiled for another target.
796
+
797
+ A solution compiled for another platform or Python version pins artifacts and
798
+ already evaluated dependencies that do not apply to this compilation. Such
799
+ solutions are returned, so callers can tell the user, but must not be used.
800
+ See `SolutionRepository.matches_target`.
801
+
802
+ Args:
803
+ solutions: Paths to solution files.
804
+ upgrade_packages: Packages to omit from the solutions.
805
+ target: The environment being compiled for. Defaults to the host.
806
+
807
+ Returns:
808
+ A repository per solution file.
809
+ """
810
+ repos = []
811
+ for solution in solutions:
812
+ repo = SolutionRepository(
813
+ solution, excluded_packages=upgrade_packages, target=target
814
+ )
815
+ if not repo.matches_target(target):
816
+ LOG.warning(
817
+ "Ignoring the solution %s, it was compiled for %s but this compilation "
818
+ "is for %s. Requirements will be resolved again.",
819
+ solution,
820
+ _recorded_target(repo),
821
+ resolve(target),
822
+ )
823
+ repos.append(repo)
824
+ return repos
825
+
826
+
758
827
  class IndentFilter(logging.Filter):
759
828
  """A filter to indent to a level specified by the depth attribute of a logging record"""
760
829
 
@@ -914,6 +983,7 @@ def compile_main(raw_args: Optional[Sequence[str]] = None) -> None:
914
983
  )
915
984
  add_logging_args(parser)
916
985
  add_repo_args(parser)
986
+ add_target_args(parser)
917
987
 
918
988
  args = parser.parse_args(args=raw_args)
919
989
  logger = logging.getLogger("req_compile")
@@ -926,6 +996,12 @@ def compile_main(raw_args: Optional[Sequence[str]] = None) -> None:
926
996
  else:
927
997
  logging.basicConfig(level=logging.CRITICAL, stream=sys.stderr)
928
998
 
999
+ try:
1000
+ target = build_target(args)
1001
+ except ValueError as ex:
1002
+ print(f"ERROR: {ex}", file=sys.stderr)
1003
+ sys.exit(1)
1004
+
929
1005
  wheeldir = args.wheel_dir
930
1006
  if wheeldir:
931
1007
  try:
@@ -1018,6 +1094,7 @@ def compile_main(raw_args: Optional[Sequence[str]] = None) -> None:
1018
1094
  extra_index_urls=args.extra_index_urls,
1019
1095
  no_index=args.no_index,
1020
1096
  allow_prerelease=args.allow_prerelease,
1097
+ target=target,
1021
1098
  )
1022
1099
  try:
1023
1100
  results, roots = perform_compile(
@@ -1027,6 +1104,7 @@ def compile_main(raw_args: Optional[Sequence[str]] = None) -> None:
1027
1104
  constraint_reqs=constraint_reqs,
1028
1105
  remove_constraints=args.remove_constraints,
1029
1106
  only_binary=args.only_binary,
1107
+ target=target,
1030
1108
  )
1031
1109
  except RepositoryInitializationError as ex:
1032
1110
  logger.exception("Error initialization repository")
@@ -1113,6 +1191,72 @@ def norm_index_url(index_url: str) -> str:
1113
1191
  return index_url.rstrip("/")
1114
1192
 
1115
1193
 
1194
+ def add_target_args(parser: argparse.ArgumentParser) -> None:
1195
+ """Add arguments describing the environment to compile for."""
1196
+ group = parser.add_argument_group(
1197
+ "target environment",
1198
+ description=(
1199
+ "Compile for an environment other than the one running req-compile. "
1200
+ "Wheels are selected and environment markers are evaluated as if "
1201
+ "installing on the target. Source distributions still have their "
1202
+ "metadata extracted on this machine, so prefer --only-binary when "
1203
+ "compiling for another platform."
1204
+ ),
1205
+ )
1206
+ group.add_argument(
1207
+ "--platform",
1208
+ dest="target_platform",
1209
+ default=None,
1210
+ metavar="platform",
1211
+ help=(
1212
+ "Platform to compile for as a wheel platform tag, as accepted by pip, e.g. "
1213
+ "manylinux_2_28_x86_64, manylinux2014_aarch64, macosx_11_0_arm64 or win_amd64. "
1214
+ "A manylinux tag accepts wheels for that glibc or older, a macosx tag wheels for "
1215
+ "that release or older. Defaults to the current platform."
1216
+ ),
1217
+ )
1218
+ group.add_argument(
1219
+ "--python-version",
1220
+ dest="target_python_version",
1221
+ default=None,
1222
+ metavar="X.Y[.Z]",
1223
+ help="Python version to compile for, e.g. 3.11. Defaults to the running interpreter.",
1224
+ )
1225
+ group.add_argument(
1226
+ "--implementation",
1227
+ dest="target_implementation",
1228
+ default=None,
1229
+ metavar="tag",
1230
+ help=(
1231
+ "Python implementation to compile for as a wheel interpreter tag, e.g. cp "
1232
+ "for CPython or pp for PyPy. Defaults to cp when --platform or "
1233
+ "--python-version is given, otherwise the running interpreter."
1234
+ ),
1235
+ )
1236
+
1237
+
1238
+ def build_target(args: argparse.Namespace) -> Optional[TargetEnvironment]:
1239
+ """Build the target environment from parsed `add_target_args` arguments.
1240
+
1241
+ Returns:
1242
+ The target, or None when compiling for the host.
1243
+
1244
+ Raises:
1245
+ ValueError: If the arguments do not describe a supported target.
1246
+ """
1247
+ if (
1248
+ args.target_platform is None
1249
+ and args.target_python_version is None
1250
+ and args.target_implementation is None
1251
+ ):
1252
+ return None
1253
+ return TargetEnvironment.from_spec(
1254
+ platform_spec=args.target_platform,
1255
+ python_version=args.target_python_version,
1256
+ implementation=args.target_implementation,
1257
+ )
1258
+
1259
+
1116
1260
  def add_repo_args(parser: argparse.ArgumentParser) -> None:
1117
1261
  """Add arguments related to adding repositories to the command line"""
1118
1262
  group = parser.add_argument_group("repositories")
@@ -17,6 +17,7 @@ from req_compile.dists import DependencyNode, DistributionCollection
17
17
  from req_compile.errors import MetadataError, NoCandidateException
18
18
  from req_compile.repos.repository import Repository
19
19
  from req_compile.repos.source import SourceRepository
20
+ from req_compile.target import TargetEnvironment
20
21
  from req_compile.utils import (
21
22
  NormName,
22
23
  is_pinned_requirement,
@@ -310,6 +311,7 @@ def perform_compile(
310
311
  allow_circular_dependencies: bool = True,
311
312
  only_binary: Optional[Set[NormName]] = None,
312
313
  max_downgrade: Optional[int] = None,
314
+ target: Optional[TargetEnvironment] = None,
313
315
  ) -> Tuple[DistributionCollection, Set[DependencyNode]]:
314
316
  """Perform a compilation using the given inputs and constraints.
315
317
 
@@ -327,11 +329,15 @@ def perform_compile(
327
329
  allow_circular_dependencies: Whether to allow circular dependencies
328
330
  only_binary: Set of projects that should only consider binary distributions.
329
331
  max_downgrade: The maximum number of version downgrades that will be allowed for conflicts.
332
+ target: The environment to compile for. Defaults to the target of `repo`, which
333
+ itself defaults to the host.
330
334
 
331
335
  Returns:
332
336
  the solution and root nodes used to generate it
333
337
  """
334
- results = req_compile.dists.DistributionCollection()
338
+ if target is None:
339
+ target = repo.target
340
+ results = req_compile.dists.DistributionCollection(target=target)
335
341
 
336
342
  constraint_nodes = set()
337
343
  nodes = set()
@@ -1,32 +1,42 @@
1
1
  import itertools
2
2
  import os
3
3
  from pathlib import Path
4
- from typing import Any, Iterable, Iterator, List, Optional, Tuple, Union
4
+ from typing import Any, Iterable, Iterator, List, Optional, Set, Tuple, Union
5
5
 
6
6
  import packaging.requirements
7
7
  import packaging.version
8
8
  from packaging.requirements import InvalidRequirement
9
9
 
10
- from req_compile.utils import reduce_requirements, req_iter_from_file
10
+ from req_compile.target import TargetEnvironment, resolve
11
+ from req_compile.utils import (
12
+ NormName,
13
+ normalize_project_name,
14
+ reduce_requirements,
15
+ req_iter_from_file,
16
+ )
11
17
 
12
18
 
13
19
  def req_uses_extra(
14
- req: packaging.requirements.Requirement, extra: Optional[str]
20
+ req: packaging.requirements.Requirement,
21
+ extra: Optional[str],
22
+ target: Optional[TargetEnvironment] = None,
15
23
  ) -> bool:
16
24
  """Determine if this requirement would be used with the given extra.
17
25
 
18
26
  If a distribution is requested with one of its extras, this filter will determine
19
27
  if the given requirement in its install requirements is provided. All base
20
28
  requirements that don't require the extra will also be included.
29
+
30
+ Environment markers other than ``extra`` are evaluated against ``target``,
31
+ which defaults to the host.
21
32
  """
22
33
  if extra and not req.marker:
23
34
  return False
24
35
  keep_req = True
25
36
  if req.marker:
26
- extras = {"extra": ""}
27
- if extra:
28
- extras = {"extra": extra}
29
- keep_req = req.marker.evaluate(extras)
37
+ keep_req = req.marker.evaluate(
38
+ resolve(target).marker_environment_with_extra(extra)
39
+ )
30
40
  return keep_req
31
41
 
32
42
 
@@ -48,17 +58,36 @@ class RequirementContainer:
48
58
  self.version: Optional[packaging.version.Version] = None
49
59
  self.hash: Optional[str] = None
50
60
  self.candidate: Any = None
61
+ # The extras that `reqs` is known to fully describe. `None` means the
62
+ # metadata is complete, so every extra the distribution provides is
63
+ # described. Solutions only record the extras that were active when they
64
+ # were compiled, so they populate this to mark the rest as unknown.
65
+ self.known_extras: Optional[Set[NormName]] = None
51
66
 
52
67
  def __iter__(self) -> Iterator[packaging.requirements.Requirement]:
53
68
  return iter(self.reqs)
54
69
 
55
70
  def requires(
56
- self, extra: Optional[str] = None
71
+ self, extra: Optional[str] = None, target: Optional[TargetEnvironment] = None
57
72
  ) -> Iterable[packaging.requirements.Requirement]:
73
+ """The requirements that apply for the given extra on the given target."""
74
+ target = resolve(target)
58
75
  return reduce_requirements(
59
- req for req in self.reqs if req_uses_extra(req, extra)
76
+ req for req in self.reqs if req_uses_extra(req, extra, target)
60
77
  )
61
78
 
79
+ def describes_extras(self, extras: Iterable[str]) -> bool:
80
+ """Whether this container describes the requirements of all the given extras.
81
+
82
+ A container built from incomplete metadata, such as a solution file, only
83
+ knows about the extras that were recorded in it. Asking it for the
84
+ requirements of any other extra silently yields nothing, so callers need
85
+ to check first and go find complete metadata instead.
86
+ """
87
+ if self.known_extras is None:
88
+ return True
89
+ return {normalize_project_name(extra) for extra in extras} <= self.known_extras
90
+
62
91
  def to_definition(
63
92
  self, extras: Optional[Iterable[str]]
64
93
  ) -> Tuple[str, Optional[packaging.version.Version]]: