11"""Nox sessions."""
2- import contextlib
2+ import hashlib
33import shutil
44import sys
5- import tempfile
65from pathlib import Path
76from textwrap import dedent
8- from typing import cast
9- from typing import Iterator
107
118import nox
129from nox .sessions import Session
1512package = "git_portfolio"
1613# python_versions = ["3.9", "3.8", "3.7"]
1714python_versions = ["3.8" , "3.7" ]
18- nox .options .sessions = "pre-commit" , "safety" , "mypy" , "tests" , "typeguard"
15+ nox .options .sessions = (
16+ "pre-commit" ,
17+ "safety" ,
18+ "mypy" ,
19+ "tests" ,
20+ "typeguard" ,
21+ "docs-build" ,
22+ )
1923
2024
2125class Poetry :
@@ -29,46 +33,62 @@ def __init__(self, session: Session) -> None:
2933 """Constructor."""
3034 self .session = session
3135
32- @contextlib .contextmanager
33- def export (self , * args : str ) -> Iterator [Path ]:
36+ def export (self , path : Path , * , dev : bool ) -> None :
3437 """Export the lock file to requirements format.
3538
3639 Args:
37- args: Command-line arguments for ``poetry export``.
38-
39- Yields:
40- The path to the requirements file.
41- """
42- with tempfile .TemporaryDirectory () as directory :
43- requirements = Path (directory ) / "requirements.txt"
44- self .session .run (
45- "poetry" ,
46- "export" ,
47- * args ,
48- "--format=requirements.txt" ,
49- f"--output={ requirements } " ,
50- external = True ,
51- )
52- yield requirements
53-
54- def version (self ) -> str :
55- """Retrieve the package version.
56-
57- Returns:
58- The package version.
40+ path: The destination path.
41+ dev: If True, include development dependencies.
5942 """
60- output = self .session .run (
61- "poetry" , "version" , external = True , silent = True , stderr = None
43+ options = ["--dev" ] if dev else []
44+ self .session .run (
45+ "poetry" ,
46+ "export" ,
47+ "--format=requirements.txt" ,
48+ f"--output={ path } " ,
49+ * options ,
50+ external = True ,
6251 )
63- return cast (str , output ).split ()[1 ]
6452
65- def build (self , * args : str ) -> None :
53+ def build (self , * args : str ) -> str :
6654 """Build the package.
6755
6856 Args:
6957 args: Command-line arguments for ``poetry build``.
58+
59+ Returns:
60+ The basename of the wheel built by Poetry.
7061 """
71- self .session .run ("poetry" , "build" , * args , external = True )
62+ output = self .session .run (
63+ "poetry" , "build" , * args , external = True , silent = True , stderr = None
64+ )
65+ assert isinstance (output , str ) # noqa: S101
66+ return output .split ()[- 1 ]
67+
68+
69+ def export_requirements (session : Session , * , dev : bool ) -> Path :
70+ """Export the lock file to requirements format.
71+
72+ Args:
73+ session: The Session object.
74+ dev: If True, include development dependencies.
75+
76+ Returns:
77+ The path to the requirements file.
78+ """
79+ tmpdir = Path (session .create_tmp ())
80+ name = "dev-requirements.txt" if dev else "requirements.txt"
81+ path = tmpdir / name
82+ hashfile = tmpdir / f"{ name } .hash"
83+
84+ lockdata = Path ("poetry.lock" ).read_bytes ()
85+ digest = hashlib .blake2b (lockdata ).hexdigest ()
86+
87+ if not hashfile .is_file () or hashfile .read_text () != digest :
88+ Poetry (session ).export (path , dev = dev )
89+ hashfile .write_text (digest )
90+
91+ return path
7292
7393
7494def install_package (session : Session ) -> None :
@@ -84,16 +104,11 @@ def install_package(session: Session) -> None:
84104 session: The Session object.
85105 """
86106 poetry = Poetry (session )
107+ wheel = poetry .build ("--format=wheel" )
108+ requirements = export_requirements (session , dev = False )
87109
88- with poetry .export () as requirements :
89- session .install (f"--requirement={ requirements } " )
90-
91- poetry .build ("--format=wheel" )
92-
93- version = poetry .version ()
94- session .install (
95- "--no-deps" , "--force-reinstall" , f"dist/{ package } -{ version } -py3-none-any.whl"
96- )
110+ session .install (f"--requirement={ requirements } " )
111+ session .install ("--no-deps" , "--force-reinstall" , f"dist/{ wheel } " )
97112
98113
99114def install (session : Session , * args : str ) -> None :
@@ -107,9 +122,8 @@ def install(session: Session, *args: str) -> None:
107122 session: The Session object.
108123 args: Command-line arguments for ``pip install``.
109124 """
110- poetry = Poetry (session )
111- with poetry .export ("--dev" ) as requirements :
112- session .install (f"--constraint={ requirements } " , * args )
125+ requirements = export_requirements (session , dev = True )
126+ session .install (f"--constraint={ requirements } " , * args )
113127
114128
115129def activate_virtualenv_in_precommit_hooks (session : Session ) -> None :
@@ -189,10 +203,9 @@ def precommit(session: Session) -> None:
189203@nox .session (python = "3.8" )
190204def safety (session : Session ) -> None :
191205 """Scan dependencies for insecure packages."""
192- poetry = Poetry (session )
193- with poetry .export ("--dev" , "--without-hashes" ) as requirements :
194- install (session , "safety" )
195- session .run ("safety" , "check" , f"--file={ requirements } " , "--bare" )
206+ install (session , "safety" )
207+ requirements = export_requirements (session , dev = True )
208+ session .run ("safety" , "check" , f"--file={ requirements } " , "--bare" )
196209
197210
198211@nox .session (python = python_versions )
@@ -210,7 +223,7 @@ def mypy(session: Session) -> None:
210223def tests (session : Session ) -> None :
211224 """Run the test suite."""
212225 install_package (session )
213- install (session , "coverage[toml]" , "pytest" , "pytest_mock" )
226+ install (session , "coverage[toml]" , "pygments" , " pytest" , "pytest_mock" )
214227 try :
215228 session .run ("coverage" , "run" , "--parallel" , "-m" , "pytest" , * session .posargs )
216229 finally :
@@ -236,7 +249,7 @@ def coverage(session: Session) -> None:
236249def typeguard (session : Session ) -> None :
237250 """Runtime type checking using Typeguard."""
238251 install_package (session )
239- install (session , "pytest " , "typeguard " , "pytest_mock" )
252+ install (session , "pygments " , "pytest " , "pytest_mock" , "typeguard " )
240253 session .run ("pytest" , f"--typeguard-packages={ package } " , * session .posargs )
241254
242255
@@ -249,22 +262,29 @@ def xdoctest(session: Session) -> None:
249262 session .run ("python" , "-m" , "xdoctest" , package , * args )
250263
251264
252- @nox .session (python = "3.8" )
253- def docs (session : Session ) -> None :
265+ @nox .session (name = "docs-build" , python = "3.8" )
266+ def docs_build (session : Session ) -> None :
254267 """Build the documentation."""
255268 args = session .posargs or ["docs" , "docs/_build" ]
269+ install_package (session )
270+ install (session , "sphinx" )
271+
272+ build_dir = Path ("docs" , "_build" )
273+ if build_dir .exists ():
274+ shutil .rmtree (build_dir )
256275
257- if session .interactive and not session .posargs :
258- args .insert (0 , "--open-browser" )
276+ session .run ("sphinx-build" , * args )
259277
260- builddir = Path ("docs" , "_build" )
261- if builddir .exists ():
262- shutil .rmtree (builddir )
263278
279+ @nox .session (python = "3.8" )
280+ def docs (session : Session ) -> None :
281+ """Build and serve the documentation with live reloading on file changes."""
282+ args = session .posargs or ["--open-browser" , "docs" , "docs/_build" ]
264283 install_package (session )
265284 install (session , "sphinx" , "sphinx-autobuild" )
266285
267- if session .interactive :
268- session .run ("sphinx-autobuild" , * args )
269- else :
270- session .run ("sphinx-build" , * args )
286+ build_dir = Path ("docs" , "_build" )
287+ if build_dir .exists ():
288+ shutil .rmtree (build_dir )
289+
290+ session .run ("sphinx-autobuild" , * args )
0 commit comments