csspin-python 2.1.1__py3-none-any.whl → 3.0.0__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.
@@ -19,7 +19,7 @@
19
19
 
20
20
  from typing import Iterable
21
21
 
22
- from csspin import Path, Verbosity, config, die, option, setenv, sh, task
22
+ from csspin import Path, Verbosity, config, die, option, setenv, sh, task, warn
23
23
  from csspin.tree import ConfigTree
24
24
 
25
25
  defaults = config(
@@ -77,6 +77,16 @@ def playwright( # pylint: disable=too-many-arguments,too-many-positional-argume
77
77
  args: Iterable[str],
78
78
  ) -> None:
79
79
  """Run the playwright tests with pytest."""
80
+ if cfg.pytest.playwright.enabled:
81
+ # This prevents the playwright tests from being run twice.
82
+ warn(
83
+ (
84
+ "The 'playwright' task has been skipped, as the playwright tests"
85
+ " are already being run by the csspin_python.pytest plugin."
86
+ " Please stop using the csspin_python.playwright plugin."
87
+ )
88
+ )
89
+ return
80
90
  setenv(
81
91
  PLAYWRIGHT_BROWSERS_PATH=cfg.playwright.browsers_path,
82
92
  PACKAGE_NAME=cfg.spin.project_name,
@@ -124,6 +134,17 @@ def _download_playwright_browsers(cfg: ConfigTree) -> None:
124
134
  )
125
135
 
126
136
 
127
- def provision(cfg: ConfigTree) -> None:
137
+ def finalize_provision(cfg: ConfigTree) -> None:
128
138
  """Install playwright browsers during provisioning"""
129
139
  _download_playwright_browsers(cfg)
140
+
141
+
142
+ def init(cfg: ConfigTree) -> None: # pylint: disable=unused-argument
143
+ """Show deprecation notice in every spin call"""
144
+ warn(
145
+ (
146
+ "The csspin_python.playwright plugin will be removed with the next major release."
147
+ " Please use csspin_python.pytest with the 'pytest.playwright.enabled=True' setting"
148
+ " instead and stop using the csspin_python.playwright plugin."
149
+ )
150
+ )
@@ -5,7 +5,9 @@
5
5
  playwright:
6
6
  type: object
7
7
  help: |
8
- The pytest plugin provides the full pytest experience for spin.
8
+ The pytest plugin provides the full pytest experience for spin. This
9
+ plugin is deprecated, use the pytest plugin with the
10
+ 'pytest.playwright.enabled=true' setting instead.
9
11
  properties:
10
12
  browsers_path:
11
13
  type: path
csspin_python/pytest.py CHANGED
@@ -20,7 +20,7 @@
20
20
 
21
21
  from typing import Iterable
22
22
 
23
- from csspin import Path, Verbosity, config, die, option, setenv, sh, task
23
+ from csspin import Path, Verbosity, config, die, interpolate1, option, setenv, sh, task
24
24
  from csspin.tree import ConfigTree
25
25
 
26
26
  defaults = config(
@@ -36,6 +36,11 @@ defaults = config(
36
36
  opts=[],
37
37
  tests=["cs", "tests"], # Strong convention @CONTACT
38
38
  test_report="pytest.xml",
39
+ playwright=config(
40
+ enabled=False,
41
+ browsers_path="{spin.data}/playwright_browsers",
42
+ browsers=["chromium"],
43
+ ),
39
44
  requires=config(
40
45
  spin=[
41
46
  "csspin_python.debugpy",
@@ -50,6 +55,24 @@ defaults = config(
50
55
  )
51
56
 
52
57
 
58
+ def _install_playwright_browsers(cfg: ConfigTree) -> None:
59
+ """Let playwright install the browsers"""
60
+ sh(
61
+ f"playwright install {' '.join(cfg.pytest.playwright.browsers)}",
62
+ env={"PLAYWRIGHT_BROWSERS_PATH": cfg.pytest.playwright.browsers_path},
63
+ )
64
+
65
+
66
+ def configure(cfg: ConfigTree) -> None:
67
+ if interpolate1(cfg.pytest.playwright.enabled).lower() == "true":
68
+ cfg.pytest.requires.python.extend(["pytest-base-url", "pytest-playwright"])
69
+
70
+
71
+ def provision(cfg: ConfigTree) -> None:
72
+ if cfg.pytest.playwright.enabled:
73
+ _install_playwright_browsers(cfg)
74
+
75
+
53
76
  @task(when="test")
54
77
  def pytest( # pylint: disable=too-many-arguments,too-many-positional-arguments
55
78
  cfg: ConfigTree,
@@ -88,6 +111,20 @@ def pytest( # pylint: disable=too-many-arguments,too-many-positional-arguments
88
111
  else:
89
112
  cmd = ["pytest"]
90
113
 
114
+ if cfg.pytest.playwright.enabled:
115
+ setenv(
116
+ PLAYWRIGHT_BROWSERS_PATH=cfg.pytest.playwright.browsers_path,
117
+ PACKAGE_NAME=cfg.spin.project_name,
118
+ )
119
+ for browser in cfg.pytest.playwright.browsers:
120
+ opts.extend(["--browser", browser])
121
+ # Run the browser download again, so that changes for
122
+ # cfg.pytest.playwright.browsers don't require a new provision call. If the
123
+ # browsers are already present it's more or less a noop.
124
+ _install_playwright_browsers(cfg)
125
+ if coverage or cfg.pytest.coverage:
126
+ setenv(PLAYWRIGHT_COVERAGE=1)
127
+
91
128
  if cfg.loaded.get("csspin_ce.mkinstance"):
92
129
  if not (
93
130
  inst := Path(instance or cfg.mkinstance.base.instance_location).absolute()
@@ -25,3 +25,22 @@ pytest:
25
25
  tests:
26
26
  type: list
27
27
  help: List of test files or directories to include.
28
+ playwright:
29
+ type: object
30
+ help: |
31
+ Settings necessary to also run playwright tests with the pytest
32
+ plugin.
33
+ properties:
34
+ enabled:
35
+ type: bool
36
+ help: |
37
+ Should the pytest-playwright plugin be installed and
38
+ initialized.
39
+ browsers_path:
40
+ type: path
41
+ help: Path for playwright to install the browsers.
42
+ browsers:
43
+ type: list
44
+ help: |
45
+ The browsers to install and to use for running the
46
+ playwright tests.
csspin_python/python.py CHANGED
@@ -68,6 +68,7 @@ point to the base installation.
68
68
 
69
69
  import abc
70
70
  import configparser
71
+ import hashlib
71
72
  import logging
72
73
  import os
73
74
  import re
@@ -152,10 +153,6 @@ defaults = config(
152
153
  python="{python.scriptdir}/python{platform.exe}",
153
154
  provisioner=None,
154
155
  provisioner_memo="{spin.spin_dir}/python_provisioner.memo",
155
- current_package=config(
156
- install=True,
157
- extras=[],
158
- ),
159
156
  aws_auth=config(
160
157
  enabled=False,
161
158
  memo="{spin.spin_dir}/aws_auth.memo",
@@ -315,13 +312,11 @@ def provision(cfg: ConfigTree) -> None:
315
312
  """Provision the python plugin"""
316
313
  with memoizer(cfg.python.provisioner_memo) as memo:
317
314
  if cfg.python.provisioner is None:
318
- cfg.python.provisioner = SimpleProvisioner()
315
+ cfg.python.provisioner = SimpleProvisioner(cfg)
319
316
  if not memo.check(cfg.python.provisioner):
320
317
  memo.add(cfg.python.provisioner)
321
318
 
322
- info("Checking {python.interpreter}")
323
319
  if not shutil.which(cfg.python.interpreter):
324
- info("Provisioning '{python.interpreter}'")
325
320
  cfg.python.provisioner.provision_python(cfg)
326
321
 
327
322
  venv_provision(cfg)
@@ -746,7 +741,7 @@ def finalize_provision(cfg: ConfigTree) -> None:
746
741
 
747
742
  class ProvisionerProtocol:
748
743
  """An implementation of this protocol is used to provision
749
- dependencies to a virtual environment.
744
+ requirements to a virtual environment.
750
745
 
751
746
  Separate plugins, can implement this interface and overwrite
752
747
  cfg.python.provisioner.
@@ -755,10 +750,8 @@ class ProvisionerProtocol:
755
750
  The provisioner will be memoized, so make sure it works with ``pickle.dumps``.
756
751
  """
757
752
 
758
- requirements: set[str]
759
- devpackages: set[str]
753
+ _requirements: set[str] = set()
760
754
 
761
- # noinspection PyMethodMayBeStatic
762
755
  def provision_python(self: Self, cfg: ConfigTree) -> None:
763
756
  """Provision the project's python interpreter"""
764
757
  if sys.platform == "win32":
@@ -770,8 +763,6 @@ class ProvisionerProtocol:
770
763
  # noinspection PyMethodMayBeStatic
771
764
  def provision_venv(self: Self, cfg: ConfigTree) -> None:
772
765
  """Provision the virtual environment of the project"""
773
- # virtualenv is guaranteed to be available like this
774
- # as we declared it as one of spin's dependencies
775
766
  cmd = [
776
767
  sys.executable,
777
768
  "-mvirtualenv",
@@ -790,38 +781,39 @@ class ProvisionerProtocol:
790
781
  def prerequisites(self: Self, cfg: ConfigTree) -> None:
791
782
  """Provide requirements for the provisioning strategy."""
792
783
 
793
- def lock(self: Self, cfg: ConfigTree) -> None:
794
- """Lock the project's dependencies."""
795
-
796
- def add(self: Self, cfg: ConfigTree, req: str, devpackage: bool = False) -> None:
797
- """Add an extra dependency (incl. development ones)."""
798
-
799
- def lock_extras(self: Self, cfg: ConfigTree) -> None:
800
- """Lock the extra dependencies."""
801
-
802
- def sync(self: Self, cfg: ConfigTree) -> None:
803
- """Synchronize the environment with the locked dependencies."""
784
+ def add(
785
+ self: Self, cfg: ConfigTree, req: str # pylint: disable=unused-argument
786
+ ) -> None:
787
+ """
788
+ Add a single requirement `req`, that will be installed into the
789
+ environment.
790
+ """
791
+ self._requirements.add(req)
804
792
 
805
793
  def install(self: Self, cfg: ConfigTree) -> None:
806
- """Install the project itself."""
794
+ """Install the requirements"""
807
795
 
808
- # noinspection PyMethodMayBeStatic
809
796
  def cleanup(self: Self, cfg: ConfigTree) -> None:
810
797
  """Cleanup the provisioned environment"""
811
798
  rmtree(cfg.python.venv)
812
799
 
813
800
 
814
801
  class SimpleProvisioner(ProvisionerProtocol):
815
- """The simplest Python dependency provisioner using pip.
802
+ """
803
+ The simplest Python provisioner, using pip.
816
804
 
817
- This provisioner will never uninstall dependencies that are no
818
- longer required.
805
+ This provisioner will never uninstall requirements that are no longer
806
+ required.
819
807
  """
820
808
 
821
- def __init__(self: Self) -> None:
822
- self.requirements = set()
823
- self.devpackages = set()
824
- self.m = Memoizer("{python.memo}")
809
+ def __init__(self: Self, cfg: ConfigTree) -> None:
810
+ self._m = Memoizer(interpolate1("{python.memo}"))
811
+ self._install_command = Command(
812
+ "pip",
813
+ None if cfg.verbosity > Verbosity.NORMAL else "-q",
814
+ "--disable-pip-version-check",
815
+ "install",
816
+ )
825
817
 
826
818
  def prerequisites(self: Self, cfg: ConfigTree) -> None:
827
819
  # We'll need pip
@@ -837,81 +829,92 @@ class SimpleProvisioner(ProvisionerProtocol):
837
829
  "pip",
838
830
  )
839
831
 
840
- def lock(self: Self, cfg: ConfigTree) -> None:
841
- """Noop"""
832
+ def install(self: Self, cfg: ConfigTree) -> None:
833
+ if requirements := self._filter(
834
+ self._requirements, self._m, cfg.spin.project_root
835
+ ):
836
+ self._install_command(*self._split(requirements))
837
+ self._m.clear()
838
+ for req in requirements:
839
+ self._m.add(_req_for_memo(req, cfg.spin.project_root))
842
840
 
843
- def add(self: Self, cfg: ConfigTree, req: str, devpackage: bool = False) -> None:
844
- # Add the requirement or devpackage if not already there.
845
- if not self.m.check(req):
846
- lst = self.devpackages if devpackage else self.requirements
847
- lst.add(req)
841
+ @staticmethod
842
+ def _split(requirements: Iterable[str]) -> list[str]:
843
+ """Used to pass whitespace-less args to :func:`csspin.sh()`."""
844
+ requirement_list = []
845
+ for requirement in requirements:
846
+ requirement_list.extend(requirement.split())
847
+ return requirement_list
848
848
 
849
- def sync(self: Self, cfg: ConfigTree) -> None:
850
- self.__execute_installation(
851
- self.requirements,
852
- None if cfg.verbosity > Verbosity.NORMAL else "-q",
853
- cfg.python.index_url,
854
- )
849
+ @staticmethod
850
+ def _filter(
851
+ requirements: set[str], memo: Memoizer, project_root: Union[Path, str]
852
+ ) -> set[str]:
853
+ """
854
+ We want to filter all requirements prior to installing them, because we
855
+ only want to run the install, when there are changes, as it takes pip
856
+ quite some time to check, whether it has to do something.
857
+ """
858
+ if all(memo.check(_req_for_memo(req, project_root)) for req in requirements):
859
+ return set()
860
+ else:
861
+ return requirements
855
862
 
856
- def install(self: Self, cfg: ConfigTree) -> None:
857
- quietflag = None if cfg.verbosity > Verbosity.NORMAL else "-q"
858
- self.__execute_installation(self.devpackages, quietflag, cfg.python.index_url)
859
863
 
860
- # If there is a setup.py, make an editable install (which
861
- # transitively also installs runtime dependencies of the project).
862
- if cfg.python.current_package.install and any(
863
- (exists("setup.py"), exists("setup.cfg"), exists("pyproject.toml"))
864
- ):
865
- cmd = [
866
- "pip",
867
- quietflag,
868
- "--disable-pip-version-check",
869
- "install",
870
- "--index-url",
871
- cfg.python.index_url,
872
- "-e",
873
- ]
874
- if cfg.python.current_package.extras:
875
- cmd.append(f".[{','.join(cfg.python.current_package.extras)}]")
876
- else:
877
- cmd.append(".")
878
- sh(*cmd)
864
+ def _file_hash(filename: Union[Path, str]) -> str:
865
+ """
866
+ Calculate a sha256 hash of a file's content and return its hexdigest.
867
+ """
868
+ with open(filename, mode="br") as fd:
869
+ return hashlib.sha256(fd.read()).hexdigest() # nosec: hashlib
870
+
871
+
872
+ def _split_requirement_option(req: str, project_root: Path) -> Union[Path, None]:
873
+ """
874
+ Takes an element of ``python.requirements`` and checks if it
875
+ is an argument for pip that contains a filename. If so,
876
+ the filename will be returned, ``None`` otherwise.
877
+
878
+ The following options are respected:
879
+ - ``-r``/``--requirement``
880
+ - ``-c``/``--constraint``
881
+
882
+ If a file for an option cannot be found, the plugin will
883
+ :func:`csspin.die()`.
884
+ """
885
+ if (
886
+ req.startswith(option := "-r")
887
+ or req.startswith(option := "--requirement")
888
+ or req.startswith(option := "-c")
889
+ or req.startswith(option := "--constraint")
890
+ ):
891
+ # The pattern has to enforce the " "/"=" for the long-options
892
+ match_many = "+" if option.startswith("--") else "*"
893
+ pattern = rf"{option}[ =]{match_many}(?P<filename>.*)"
894
+ match = re.match(pattern, req)
895
+ if not match:
896
+ die(f"{req} could not be validated.")
897
+ else:
898
+ file = project_root / match.group("filename")
899
+ if not file.exists():
900
+ die(f"{file} does not exist.")
901
+ return file
902
+ return None
879
903
 
880
- # Verify dependency compatibility of installed packages
881
- pip_check = sh(
882
- "pip",
883
- "--disable-pip-version-check",
884
- "check",
885
- check=False,
886
- capture_output=True,
887
- )
888
- if pip_check.returncode:
889
- die(pip_check.stdout)
890
-
891
- def _split(self: Self, reqset: set[str]) -> list[str]:
892
- """to pass whitespace-less args to sh()"""
893
- reqlist = []
894
- for req in reqset:
895
- reqlist.extend(req.split())
896
- return reqlist
897
-
898
- def __execute_installation(
899
- self: Self, packages: set[str], quietflag: Union[str, None], index_url: str
900
- ) -> None:
901
- """Install packages that are not yet memoized"""
902
- if to_install := {package for package in packages if not self.m.check(package)}:
903
- sh(
904
- "pip",
905
- quietflag,
906
- "--disable-pip-version-check",
907
- "install",
908
- "--index-url",
909
- index_url,
910
- *self._split(to_install),
911
- )
912
- for package in to_install:
913
- self.m.add(package)
914
- self.m.save()
904
+
905
+ def _req_for_memo(
906
+ req: str, project_root: Union[Path, str]
907
+ ) -> str: # pylint: disable=inconsistent-return-statements
908
+ """
909
+ Return a memoizable representation of a python requirement. In case a
910
+ requirement is on of the following options, the function returns requirement
911
+ with a hash of the files' content appended. Otherwise the requirement itself
912
+ will be returned.
913
+ """
914
+ if file := _split_requirement_option(req, project_root):
915
+ return f"{req}{_file_hash(file)}"
916
+ else:
917
+ return req
915
918
 
916
919
 
917
920
  def venv_provision( # pylint: disable=too-many-branches,missing-function-docstring
@@ -946,16 +949,10 @@ def venv_provision( # pylint: disable=too-many-branches,missing-function-docstr
946
949
  logging.debug(f"{plugin_module.__name__}.venv_hook()")
947
950
  hook(cfg)
948
951
 
949
- cfg.python.provisioner.lock(cfg)
950
-
951
- # Install packages required by the project ('requirements')
952
+ # Add packages required by the project ('requirements')
952
953
  for req in cfg.python.get("requirements", []):
953
954
  cfg.python.provisioner.add(cfg, interpolate1(req))
954
955
 
955
- # Install development packages required by the project ('devpackages')
956
- for pkgspec in cfg.python.get("devpackages", []):
957
- cfg.python.provisioner.add(cfg, interpolate1(pkgspec), True)
958
-
959
956
  # Install packages required by plugins used
960
957
  # ('<plugin>.requires.python')
961
958
  for plugin in cfg.spin.topo_plugins:
@@ -963,9 +960,6 @@ def venv_provision( # pylint: disable=too-many-branches,missing-function-docstr
963
960
  for req in get_requires(plugin_module.defaults, "python"):
964
961
  cfg.python.provisioner.add(cfg, interpolate1(req))
965
962
 
966
- cfg.python.provisioner.lock_extras(cfg)
967
- cfg.python.provisioner.sync(cfg)
968
-
969
963
 
970
964
  def cleanup(cfg: ConfigTree) -> None:
971
965
  """Remove directories and files generated by the python plugin."""
@@ -1046,7 +1040,9 @@ def _check_aws_token_validity(cfg: ConfigTree) -> None:
1046
1040
  for item in memo.items():
1047
1041
  if isinstance(item, str) and item.startswith(f"{timestamp_key}:"):
1048
1042
  last_time = int(item.split(":", 1)[1])
1049
- if current_time - last_time < cfg.python.aws_auth.key_duration:
1043
+ if current_time - last_time < int(
1044
+ interpolate1(cfg.python.aws_auth.key_duration)
1045
+ ):
1050
1046
  pipconf = _get_pipconf(cfg)
1051
1047
  config_parser = configparser.ConfigParser()
1052
1048
  config_parser.read(pipconf)
@@ -1064,12 +1060,18 @@ def _check_aws_token_validity(cfg: ConfigTree) -> None:
1064
1060
  info("Updating Codeartifact token.")
1065
1061
  from urllib.parse import urljoin
1066
1062
 
1063
+ opts = {
1064
+ "static_oidc": interpolate1(cfg.python.aws_auth.static_oidc).lower()
1065
+ == "true"
1066
+ }
1067
+ if cfg.python.aws_auth.client_id:
1068
+ opts["client_id"] = interpolate1(cfg.python.aws_auth.client_id)
1069
+ if cfg.python.aws_auth.role_arn:
1070
+ opts["aws_role_arn"] = interpolate1(cfg.python.aws_auth.role_arn)
1071
+
1072
+ index_base_url = get_ca_pypi_url_programmatic(**opts)
1067
1073
  index_url = urljoin(
1068
- get_ca_pypi_url_programmatic(
1069
- static_oidc=cfg.python.aws_auth.static_oidc
1070
- )
1071
- + "/",
1072
- cfg.python.aws_auth.index,
1074
+ index_base_url + "/", interpolate1(cfg.python.aws_auth.index)
1073
1075
  )
1074
1076
  cfg.python.index_url = index_url
1075
1077
  _obfuscate_index_url(index_url)
@@ -96,9 +96,6 @@ python:
96
96
  site_packages:
97
97
  type: path internal
98
98
  help: The path to the virtual environments site-packages directory.
99
- devpackages:
100
- type: list
101
- help: A list of packages that will be editable installed
102
99
  requirements:
103
100
  type: list
104
101
  help: |
@@ -106,18 +103,8 @@ python:
106
103
  have to be installed into the project's virtual environment.
107
104
  This excludes requirements stated as 'install_requires' in
108
105
  setup.py/setup.cfg.
109
- current_package:
110
- type: object
111
- help: Install configuration of the current python project
112
- properties:
113
- install:
114
- type: bool
115
- help: |
116
- Property to determine whether the current package should
117
- get installed or not.
118
- extras:
119
- type: list
120
- help: The extras of the current package to install.
106
+ Note that editable installs with ``-e`` or requirement files
107
+ with ``-r`` can also be used here.
121
108
  index_url:
122
109
  type: str
123
110
  help: |
@@ -157,3 +144,9 @@ python:
157
144
  index:
158
145
  type: str
159
146
  help: The Codeartifact repository index (e.g. "16.0/simple").
147
+ client_id:
148
+ type: str
149
+ help: The OIDC client ID to use.
150
+ role_arn:
151
+ type: str
152
+ help: The role ARN to assume when authenticating.
@@ -0,0 +1,187 @@
1
+ # -*- mode: python; coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2025 CONTACT Software GmbH
4
+ # https://www.contact-software.com/
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ """
19
+ Plugin to replace certain things of the csspin_python.python plugin with the
20
+ tool ``uv``. Can only be used if the ``uv`` extra of csspin_python has been
21
+ installed.
22
+ """
23
+
24
+ import shutil
25
+ import subprocess
26
+ from typing import Union
27
+
28
+ try:
29
+ import tomllib
30
+ except ImportError:
31
+ # Fallback for spin has been installed with python < 3.11
32
+ import tomli as tomllib
33
+
34
+ import tomli_w
35
+ from csspin import Command, Path, Verbosity, config, die, info, interpolate1, setenv
36
+ from csspin.tree import ConfigTree
37
+
38
+ from csspin_python.python import SimpleProvisioner
39
+
40
+ defaults = config(
41
+ enabled=False,
42
+ uv_python_data="{spin.data}/uv_python",
43
+ uv_toml_path="{python.venv}/uv.toml",
44
+ requires=config(
45
+ spin=[
46
+ "csspin_python.python",
47
+ ],
48
+ ),
49
+ )
50
+
51
+
52
+ def venv_hook(cfg: ConfigTree) -> None:
53
+ """Things to do right after venv creation."""
54
+ _configure_uv_toml(cfg)
55
+ setenv(UV_CONFIG_FILE=cfg.uv_provisioner.uv_toml_path)
56
+
57
+
58
+ def configure(cfg: ConfigTree) -> None:
59
+ """Configure the uv_provisioner plugin."""
60
+ if interpolate1(cfg.uv_provisioner.enabled).lower() == "true":
61
+ cfg.python.provisioner = SimpleUvProvisioner(cfg)
62
+ setenv(
63
+ UV_PYTHON_INSTALL_DIR=interpolate1(cfg.uv_provisioner.uv_python_data),
64
+ )
65
+ if cfg.python.use:
66
+ cfg.python.interpreter = shutil.which(interpolate1(cfg.python.interpreter))
67
+ else:
68
+ if interpreter_path := _get_uv_python(cfg, True):
69
+ cfg.python.interpreter = interpreter_path
70
+ else:
71
+ # No uv provisioned python found, set to an empty string to
72
+ # force provisioning
73
+ cfg.python.interpreter = ""
74
+
75
+ if cfg.python.aws_auth.enabled:
76
+ # In case we use aws_auth the index-url might have changed
77
+ _update_index_url_in_toml(cfg)
78
+
79
+
80
+ def _get_uv_python(cfg: ConfigTree, ignore_errors: bool = False) -> Union[Path, None]:
81
+ """Use uv to find its provisioned python interpreter."""
82
+ # We cannot put this import top-level as "spin cleanup" might not work
83
+ # otherwise.
84
+ from uv import find_uv_bin
85
+
86
+ cmd = [
87
+ find_uv_bin(),
88
+ "python",
89
+ "find",
90
+ "--no-project",
91
+ "--system",
92
+ "--managed-python",
93
+ cfg.python.version,
94
+ ]
95
+ try:
96
+ out = subprocess.check_output(
97
+ cmd, encoding="utf-8", stderr=subprocess.DEVNULL if ignore_errors else None
98
+ )
99
+ interpreter = out.strip()
100
+ return Path(interpreter)
101
+ except subprocess.CalledProcessError as ex:
102
+ if not ignore_errors:
103
+ die(ex)
104
+ return None
105
+
106
+
107
+ class SimpleUvProvisioner(SimpleProvisioner):
108
+ """
109
+ Drop-in replacement for the SimpleProvisioner that uses ``uv`` for creating
110
+ the environment and installing the requirements.
111
+
112
+ Especially when installing Python packages, the ``SimpleUvProvisioner`` is
113
+ much faster than the ``SimpleProvisioner``.
114
+ """
115
+
116
+ def __init__(self, cfg: ConfigTree) -> None:
117
+ super().__init__(cfg)
118
+
119
+ from uv import find_uv_bin
120
+
121
+ uv_bin = find_uv_bin()
122
+
123
+ if cfg.verbosity == Verbosity.QUIET:
124
+ verbosity = "-q"
125
+ elif cfg.verbosity == Verbosity.DEBUG:
126
+ verbosity = "-v"
127
+ else:
128
+ verbosity = None
129
+
130
+ self._uv_cmd = Command(
131
+ uv_bin,
132
+ verbosity,
133
+ )
134
+ self._install_command = Command(
135
+ *self._uv_cmd._cmd,
136
+ "pip",
137
+ "install",
138
+ )
139
+
140
+ def provision_python(self, cfg: ConfigTree) -> None:
141
+ self._uv_cmd(
142
+ "python",
143
+ "install",
144
+ cfg.python.version,
145
+ "--no-bin",
146
+ "--no-registry",
147
+ )
148
+ cfg.python.interpreter = _get_uv_python(cfg)
149
+ info(f"Using '{cfg.python.interpreter}' as interpreter")
150
+
151
+ def provision_venv(self, cfg: ConfigTree) -> None:
152
+ setenv(UV_PROJECT_ENVIRONMENT=cfg.python.venv)
153
+ self._uv_cmd(
154
+ "venv",
155
+ f"--python={cfg.python.interpreter}",
156
+ cfg.python.venv,
157
+ )
158
+
159
+ def prerequisites(self, cfg: ConfigTree) -> None:
160
+ self._uv_cmd("pip", "install", "pip")
161
+
162
+
163
+ def _configure_uv_toml(cfg: ConfigTree) -> None:
164
+ """
165
+ Create a config file for uv, similar to the pip.conf of
166
+ csspin_python.python, since `uv` pip won't respect the pip.conf.
167
+ """
168
+ toml_content = tomllib.loads(cfg.uv_provisioner.uv_toml or "")
169
+ if "index-url" not in toml_content:
170
+ toml_content["index-url"] = cfg.python.index_url
171
+ else:
172
+ toml_content["index-url"] = toml_content.get("index-url", cfg.python.index_url)
173
+
174
+ with open(cfg.uv_provisioner.uv_toml_path, mode="wb") as fd:
175
+ tomli_w.dump(toml_content, fd)
176
+
177
+
178
+ def _update_index_url_in_toml(cfg: ConfigTree) -> None:
179
+ """
180
+ Update the index-url in the uv.toml in case it changed.
181
+ """
182
+ if (uv_toml_path := interpolate1(cfg.uv_provisioner.uv_toml_path)).exists():
183
+ with open(uv_toml_path, mode="r+b") as fd:
184
+ toml_content = tomllib.load(fd)
185
+ if toml_content.get("index-url") != cfg.python.index_url:
186
+ toml_content["index-url"] = cfg.python.index_url
187
+ tomli_w.dump(toml_content, fd)
@@ -0,0 +1,24 @@
1
+ # -*- mode: yaml; coding: utf-8 -*-
2
+ #
3
+ # Schema for the uv_provisioner plugin for spin
4
+
5
+ uv_provisioner:
6
+ type: object
7
+ help: Configuration of the csspin_python.uv plugin
8
+ properties:
9
+ enabled:
10
+ type: bool
11
+ help: |
12
+ Used to let the python plugin use the SimpleUvProvisioner to
13
+ provision the python environment.
14
+ uv_python_data:
15
+ type: path
16
+ help: |
17
+ The directory where the python interpreters uv provisions are
18
+ being stored.
19
+ uv_toml_path:
20
+ type: path
21
+ help: Path to uv's config file
22
+ uv_toml:
23
+ type: str
24
+ help: Content for uv's config file
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: csspin-python
3
- Version: 2.1.1
3
+ Version: 3.0.0
4
4
  Summary: Plugin-package for csspin providing Python related plugins
5
5
  Author-email: CONTACT Software GmbH <info@contact-software.com>
6
6
  Maintainer-email: Waleri Enns <waleri.enns@contact-software.com>, Benjamin Thomas Schwertfeger <benjaminthomas.schwertfeger@contact-software.com>, Fabian Hafer <fabian.hafer@contact-software.com>
@@ -24,6 +24,10 @@ Requires-Dist: platformdirs~=4.3.8
24
24
  Requires-Dist: virtualenv
25
25
  Provides-Extra: aws-auth
26
26
  Requires-Dist: csaccess>=0.1.0; extra == "aws-auth"
27
+ Provides-Extra: uv
28
+ Requires-Dist: tomli; python_version < "3.11" and extra == "uv"
29
+ Requires-Dist: tomli-w; extra == "uv"
30
+ Requires-Dist: uv; extra == "uv"
27
31
  Dynamic: license-file
28
32
 
29
33
  |Latest Version| |Python| |License|
@@ -36,16 +40,20 @@ The following plugins are available:
36
40
  - `csspin_python.behave`: A plugin for running tests using Behave.
37
41
  - `csspin_python.debugpy`: A plugin for debugging Python code using `debugpy`_.
38
42
  - `csspin_python.devpi`: A plugin for simplified usage of `devpi`_.
39
- - `csspin_python.playwright`: A plugin for running tests using `playwright`_.
40
43
  - `csspin_python.pytest`: A plugin for running tests using pytest.
41
44
  - `csspin_python.python`: A plugin for provisioning Python environments and
42
45
  installing dependencies.
43
46
  - `csspin_python.radon`: A plugin for running `radon`_ to analyze code
44
47
  complexity.
45
48
  - `csspin_python.sphinx`: A plugin for building Sphinx documentation.
49
+ - `csspin_python.playwright`: A plugin for running tests using `playwright`_.
50
+ This plugin is deprecated, use the pytest plugin with the
51
+ 'pytest.playwright.enabled=true' setting instead.
52
+ - `csspin_python.uv_provisioner`: A plugin that uses `uv`_ to provision the Python environment.
46
53
 
47
- The package provides an ``aws_auth`` extra, that, if enabled, can
48
- authenticate to `CONTACT Software GmbH`_'s AWS Codeartifact.
54
+ The package provides an ``aws_auth`` extra, that, if enabled, can authenticate
55
+ to `CONTACT Software GmbH`_'s AWS Codeartifact. It also provides an ``uv``
56
+ extra, that is necessary for using the ``csspin_python.uv_provisioner`` plugin.
49
57
 
50
58
  Prerequisites
51
59
  -------------
@@ -108,3 +116,4 @@ tests using ``spin pytest`` and do other great things.
108
116
  .. _`devpi`: https://pypi.org/project/devpi
109
117
  .. _`playwright`: https://pypi.org/project/pytest-playwright
110
118
  .. _`radon`: https://pypi.org/project/radon
119
+ .. _`uv`: https://docs.astral.sh/uv/
@@ -0,0 +1,21 @@
1
+ csspin_python/behave.py,sha256=iJZeyIqB7V_NzTdLTZldNY9W_GGwCWkXe6WY69wpDqs,4997
2
+ csspin_python/behave_schema.yaml,sha256=8qoOCK-uTmwgRRW29urgK0X_kgn0zO0X34v89bvii2w,1241
3
+ csspin_python/debugpy.py,sha256=v0ZZopv5TNoSaFf2kiePsw9OmhBpjfOBFh0u71jTcnQ,962
4
+ csspin_python/debugpy_schema.yaml,sha256=BeH30nSirDYctkdhS9xMXUG5htj3PED_ZjmxPG5WRUc,364
5
+ csspin_python/devpi.py,sha256=C-5O_vA06CwQR4uElOw-2VH2-m001SpxowM_X6RbRwo,2352
6
+ csspin_python/devpi_schema.yaml,sha256=2gPATWjVcfvCTrGZX2FK6wH8hh9KS0XzZ35JvZeJGEU,487
7
+ csspin_python/playwright.py,sha256=oFfphLqa4AB6K9vasCUFHN0kFXu63n3ocrsqVuRp4-0,5102
8
+ csspin_python/playwright_schema.yaml,sha256=TSeR16YHa7m7bfO59F2eMV-jXcglluTJdEpUeL16saY,1178
9
+ csspin_python/pytest.py,sha256=fX50f22DYtJNNTBkAt75ki26eM0sZDcRXnwoFsKWV_M,4565
10
+ csspin_python/pytest_schema.yaml,sha256=tzXtdF6MvGC9v59EVRJFfLeMMHqPsXcFXy2zJtRECBI,1535
11
+ csspin_python/python.py,sha256=SRsl7wXkPFxEqwhjDtr-Uq80N_JNRrlD0kPMaE1gfrU,34610
12
+ csspin_python/python_schema.yaml,sha256=s8snEDJ8UdfpORgCgqbKvy0exaXlvy4U1gUwBd-Do94,5739
13
+ csspin_python/radon.py,sha256=uFqm6FEi5oWj-_XVaAm3s9cam0cUmr1_FwRf40K6xWs,1876
14
+ csspin_python/radon_schema.yaml,sha256=rlRzXw5z4XbjOVznRiUxWGP4E9hx1Jm-gGw1iQiYzE0,548
15
+ csspin_python/uv_provisioner.py,sha256=A6Di0ahCrZCO3KhYedry7JCfa5ME6h7vH4ypRBZh5UA,5907
16
+ csspin_python/uv_provisioner_schema.yaml,sha256=Y8ZNC2OMnhR8Us3WUXAXK9hMjqGWAKFJB2puX4X5XNQ,727
17
+ csspin_python-3.0.0.dist-info/licenses/LICENSE,sha256=4MAecetnRTQw5DlHtiikDSzKWO1xVLwzM5_DsPMYlnE,10172
18
+ csspin_python-3.0.0.dist-info/METADATA,sha256=NkiY16n49Q9lFYCu-emb9G1OjQlg3gEm0vW5t3EJwtE,4715
19
+ csspin_python-3.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
+ csspin_python-3.0.0.dist-info/top_level.txt,sha256=QSeglMEGbFu1z4L6MCQYwo01NgL0KojWvC4rzgMQ8gU,14
21
+ csspin_python-3.0.0.dist-info/RECORD,,
@@ -1,19 +0,0 @@
1
- csspin_python/behave.py,sha256=iJZeyIqB7V_NzTdLTZldNY9W_GGwCWkXe6WY69wpDqs,4997
2
- csspin_python/behave_schema.yaml,sha256=8qoOCK-uTmwgRRW29urgK0X_kgn0zO0X34v89bvii2w,1241
3
- csspin_python/debugpy.py,sha256=v0ZZopv5TNoSaFf2kiePsw9OmhBpjfOBFh0u71jTcnQ,962
4
- csspin_python/debugpy_schema.yaml,sha256=BeH30nSirDYctkdhS9xMXUG5htj3PED_ZjmxPG5WRUc,364
5
- csspin_python/devpi.py,sha256=C-5O_vA06CwQR4uElOw-2VH2-m001SpxowM_X6RbRwo,2352
6
- csspin_python/devpi_schema.yaml,sha256=2gPATWjVcfvCTrGZX2FK6wH8hh9KS0XzZ35JvZeJGEU,487
7
- csspin_python/playwright.py,sha256=mzSsLcmewDZnZwdSyp5HytEMnXgkoJ9s1XXkd05eOwU,4254
8
- csspin_python/playwright_schema.yaml,sha256=WFMok7dB7G6L8f8y_2_RKHjGe4ww1iUUS4tqCoUI1FE,1054
9
- csspin_python/pytest.py,sha256=Mx6l4Cb28FjdZgL9Vd1zBbhcnYyc4QsqwkozksoKZJc,3189
10
- csspin_python/pytest_schema.yaml,sha256=1bF8hNsJfV-LHUwGBBJ3GnQOZJiIQkG81DCBma2MalU,809
11
- csspin_python/python.py,sha256=AbIX3r07c4tVAj0jIfjiRi7Vk9XFiap6AvS4xCYee48,34461
12
- csspin_python/python_schema.yaml,sha256=F_PMK8D3KBvXK945b6-oRDoaxuDgxkBGqVPAJ-eFmv0,5970
13
- csspin_python/radon.py,sha256=uFqm6FEi5oWj-_XVaAm3s9cam0cUmr1_FwRf40K6xWs,1876
14
- csspin_python/radon_schema.yaml,sha256=rlRzXw5z4XbjOVznRiUxWGP4E9hx1Jm-gGw1iQiYzE0,548
15
- csspin_python-2.1.1.dist-info/licenses/LICENSE,sha256=4MAecetnRTQw5DlHtiikDSzKWO1xVLwzM5_DsPMYlnE,10172
16
- csspin_python-2.1.1.dist-info/METADATA,sha256=pIuulk5YG9JUQqORop0oV4GoX0DdHZoOOGktz8YkhYA,4209
17
- csspin_python-2.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
18
- csspin_python-2.1.1.dist-info/top_level.txt,sha256=QSeglMEGbFu1z4L6MCQYwo01NgL0KojWvC4rzgMQ8gU,14
19
- csspin_python-2.1.1.dist-info/RECORD,,