-
Notifications
You must be signed in to change notification settings - Fork 20
/
Copy pathsessions.py
276 lines (203 loc) · 9.73 KB
/
sessions.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
"""Replacements for the ``nox.session`` decorator and the ``nox.Session`` class."""
import functools
import hashlib
import re
from pathlib import Path
from typing import Any
from typing import Iterable
from typing import Iterator
from typing import Optional
from typing import Tuple
import nox
from packaging.requirements import InvalidRequirement
from packaging.requirements import Requirement
from nox_poetry.poetry import DistributionFormat
from nox_poetry.poetry import Poetry
def session(*args: Any, **kwargs: Any) -> Any:
"""Drop-in replacement for the :func:`nox.session` decorator.
Use this decorator instead of ``@nox.session``. Session functions are passed
:class:`Session` instead of :class:`nox.sessions.Session`; otherwise, the
decorators work exactly the same.
Args:
args: Positional arguments are forwarded to ``nox.session``.
kwargs: Keyword arguments are forwarded to ``nox.session``.
Returns:
The decorated session function.
"""
if not args:
return functools.partial(session, **kwargs)
[function] = args
@functools.wraps(function)
def wrapper(session: nox.Session, *_args, **_kwargs) -> None:
proxy = Session(session)
function(proxy, *_args, **_kwargs)
return nox.session(wrapper, **kwargs) # type: ignore[call-overload]
_EXTRAS_PATTERN = re.compile(r"^(.+)(\[[^\]]+\])$")
def _split_extras(arg: str) -> Tuple[str, Optional[str]]:
# From ``pip._internal.req.constructors._strip_extras``
match = _EXTRAS_PATTERN.match(arg)
if match:
return match.group(1), match.group(2)
return arg, None
def to_constraint(requirement_string: str, line: int) -> Optional[str]:
"""Convert requirement to constraint."""
if any(
requirement_string.startswith(prefix)
for prefix in ("-", "file://", "git+https://", "http://", "https://")
):
return None
try:
requirement = Requirement(requirement_string)
except InvalidRequirement as error:
raise RuntimeError(f"line {line}: {requirement_string!r}: {error}")
if not (requirement.name and requirement.specifier):
return None
constraint = f"{requirement.name}{requirement.specifier}"
return f"{constraint}; {requirement.marker}" if requirement.marker else constraint
def to_constraints(requirements: str) -> str:
"""Convert requirements to constraints."""
def _to_constraints() -> Iterator[str]:
lines = requirements.splitlines()
for line, requirement in enumerate(lines, start=1):
if requirement.strip():
constraint = to_constraint(requirement, line)
if constraint is not None:
yield constraint
return "\n".join(_to_constraints())
class _PoetrySession:
"""Poetry-related utilities for session functions."""
def __init__(self, session: nox.Session) -> None:
"""Initialize."""
self.session = session
self.poetry = Poetry(session)
def install(self, *args: str, **kwargs: Any) -> None:
"""Install packages into a Nox session using Poetry.
This function installs packages into the session's virtual environment. It
is a wrapper for :meth:`nox.sessions.Session.install`, whose positional
arguments are command-line arguments for :ref:`pip install`, and whose keyword
arguments are the same as those for :meth:`nox.sessions.Session.run`.
If a positional argument is ".", a wheel is built using
:meth:`build_package`, and the argument is replaced with the file URL
returned by that function. Otherwise, the argument is forwarded unchanged.
In addition, a :ref:`constraints file <Constraints Files>` is generated
for the package dependencies using :meth:`export_requirements`, and
passed to ``pip install`` via its ``--constraint`` option. This ensures
that any package installed will be at the version specified in Poetry's
lock file.
Args:
args: Command-line arguments for ``pip install``.
kwargs: Keyword-arguments for ``session.install``. These are the same
as those for :meth:`nox.sessions.Session.run`.
"""
from nox_poetry.core import Session_install
args_extras = [_split_extras(arg) for arg in args]
if "." in [arg for arg, _ in args_extras]:
package = self.build_package()
def rewrite(arg: str, extras: Optional[str]) -> str:
if arg != ".":
return arg if extras is None else arg + extras
if extras is None:
return package
name = self.poetry.config.name
return f"{name}{extras} @ {package}"
args = tuple(rewrite(arg, extras) for arg, extras in args_extras)
self.session.run_always("pip", "uninstall", "--yes", package, silent=True)
requirements = self.export_requirements()
Session_install(self.session, f"--constraint={requirements}", *args, **kwargs)
def installroot(
self,
*,
distribution_format: str = DistributionFormat.WHEEL,
extras: Iterable[str] = (),
) -> None:
"""Install the root package into a Nox session using Poetry.
This function installs the package located in the current directory into the
session's virtual environment.
A :ref:`constraints file <Constraints Files>` is generated for the
package dependencies using :meth:`export_requirements`, and passed to
:ref:`pip install` via its ``--constraint`` option. This ensures that
core dependencies are installed using the versions specified in Poetry's
lock file.
Args:
distribution_format: The distribution format, either wheel or sdist.
extras: Extras to install for the package.
"""
from nox_poetry.core import Session_install
package = self.build_package(distribution_format=distribution_format)
requirements = self.export_requirements()
self.session.run_always("pip", "uninstall", "--yes", package, silent=True)
suffix = ",".join(extras)
if suffix.strip():
suffix = suffix.join("[]")
name = self.poetry.config.name
package = f"{name}{suffix} @ {package}"
Session_install(self.session, f"--constraint={requirements}", package)
def export_requirements(self) -> Path:
"""Export a requirements file from Poetry.
This function uses `poetry export`_ to generate a :ref:`requirements
file <Requirements Files>` containing the project dependencies at the
versions specified in ``poetry.lock``. The requirements file includes
both core and development dependencies.
The requirements file is stored in a per-session temporary directory,
together with a hash digest over ``poetry.lock`` to avoid generating the
file when the dependencies have not changed since the last run.
.. _poetry export: https://python-poetry.org/docs/cli/#export
Returns:
The path to the requirements file.
"""
tmpdir = Path(self.session.virtualenv.location) / "tmp"
tmpdir.mkdir(exist_ok=True)
path = tmpdir / "requirements.txt"
hashfile = tmpdir / f"{path.name}.hash"
lockdata = Path("poetry.lock").read_bytes()
digest = hashlib.blake2b(lockdata).hexdigest()
if not hashfile.is_file() or hashfile.read_text() != digest:
constraints = to_constraints(self.poetry.export())
path.write_text(constraints)
hashfile.write_text(digest)
return path
def build_package(
self, *, distribution_format: str = DistributionFormat.WHEEL
) -> str:
"""Build a distribution archive for the package.
This function uses `poetry build`_ to build a wheel or sdist archive for
the local package, as specified via the ``distribution_format`` parameter.
It returns a file URL with the absolute path to the built archive.
.. _poetry build: https://python-poetry.org/docs/cli/#export
Args:
distribution_format: The distribution format, either wheel or sdist.
Returns:
The file URL for the distribution package.
"""
wheel = Path("dist") / self.poetry.build(format=distribution_format)
url = wheel.resolve().as_uri()
if DistributionFormat(distribution_format) is DistributionFormat.SDIST:
url += f"#egg={self.poetry.config.name}"
return url
class _SessionProxy:
"""Proxy for :class:`nox.sessions.Session`."""
def __init__(self, session: nox.Session) -> None:
"""Initialize."""
self._session = session
def __getattr__(self, name: str) -> Any:
"""Delegate attribute access to nox.Session."""
return getattr(self._session, name)
class Session(_SessionProxy):
"""Proxy for :class:`nox.sessions.Session`, passed to session functions.
This class overrides :meth:`session.install
<nox_poetry.sessions._PoetrySession.install>`, and provides Poetry-related
utilities:
- :meth:`Session.poetry.installroot
<nox_poetry.sessions._PoetrySession.installroot>`
- :meth:`Session.poetry.build_package
<nox_poetry.sessions._PoetrySession.build_package>`
- :meth:`Session.poetry.export_requirements
<nox_poetry.sessions._PoetrySession.export_requirements>`
"""
def __init__(self, session: nox.Session) -> None:
"""Initialize."""
super().__init__(session)
self.poetry = _PoetrySession(session)
def install(self, *args: str, **kwargs: Any) -> None:
"""Install packages into a Nox session using Poetry."""
return self.poetry.install(*args, **kwargs)