11"""Nox sessions.""" 
22
33import  os 
4+ import  shlex 
45import  shutil 
56import  sys 
67from  pathlib  import  Path 
78from  tempfile  import  NamedTemporaryFile 
9+ from  textwrap  import  dedent 
810
911import  nox 
1012from  nox  import  Session 
2527)
2628
2729
30+ def  session_install_uv (
31+     session : Session ,
32+     install_project : bool  =  True ,
33+     install_dev : bool  =  False ,
34+     install_docs : bool  =  False ,
35+ ) ->  None :
36+     """Install root project into the session's virtual environment using uv.""" 
37+     env  =  {"UV_PROJECT_ENVIRONMENT" : session .virtualenv .location }
38+ 
39+     args  =  ["uv" , "sync" , "--frozen" ]
40+     if  not  install_project :
41+         args .append ("--no-install-project" )
42+     if  not  install_dev :
43+         args .append ("--no-dev" )
44+     if  install_docs :
45+         args .extend (["--group" , "docs" ])
46+ 
47+     session .run_install (* args , silent = True , env = env )
48+ 
49+ 
50+ def  session_install_uv_package (session : Session , packages : list [str ]) ->  None :
51+     """Install packages into the session's virtual environment using uv lockfile.""" 
52+     env  =  {"UV_PROJECT_ENVIRONMENT" : session .virtualenv .location }
53+ 
54+     # Export requirements.txt to session temp dir using uv with locked dependencies 
55+     requirements_tmp  =  str (Path (session .create_tmp ()) /  "requirements.txt" )
56+     export_args  =  ["uv" , "export" , "--only-dev" , "--no-hashes" , "-o" , requirements_tmp ]
57+     session .run_install (* export_args , silent = True , env = env )
58+ 
59+     # Install requested packages with requirements.txt constraints 
60+     session .install (* packages , "--constraint" , requirements_tmp )
61+ 
62+ 
63+ def  activate_virtualenv_in_precommit_hooks (session : Session ) ->  None :
64+     """Activate virtualenv in hooks installed by pre-commit. 
65+ 
66+     This function patches git hooks installed by pre-commit to activate the 
67+     session's virtual environment. This allows pre-commit to locate hooks in 
68+     that environment when invoked from git. 
69+ 
70+     Args: 
71+         session: The Session object. 
72+     """ 
73+     assert  session .bin  is  not   None   # noqa: S101 
74+ 
75+     # Only patch hooks containing a reference to this session's bindir. Support 
76+     # quoting rules for Python and bash, but strip the outermost quotes so we 
77+     # can detect paths within the bindir, like <bindir>/python. 
78+     bindirs  =  [
79+         bindir [1 :- 1 ] if  bindir [0 ] in  "'\" "  else  bindir 
80+         for  bindir  in  (repr (session .bin ), shlex .quote (session .bin ))
81+     ]
82+ 
83+     virtualenv  =  session .env .get ("VIRTUAL_ENV" )
84+     if  virtualenv  is  None :
85+         return 
86+ 
87+     headers  =  {
88+         # pre-commit < 2.16.0 
89+         "python" : f"""\  
90+              import os
91+             os.environ["VIRTUAL_ENV"] = { virtualenv !r}  
92+             os.environ["PATH"] = os.pathsep.join(( 
93+                 { session .bin !r}  , 
94+                 os.environ.get("PATH", ""), 
95+             )) 
96+             """ ,
97+         # pre-commit >= 2.16.0 
98+         "bash" : f"""\  
99+              VIRTUAL_ENV={ shlex .quote (virtualenv )} 
100+             PATH={ shlex .quote (session .bin )}  "{ os .pathsep }  $PATH" 
101+             """ ,
102+         # pre-commit >= 2.17.0 on Windows forces sh shebang 
103+         "/bin/sh" : f"""\  
104+              VIRTUAL_ENV={ shlex .quote (virtualenv )} 
105+             PATH={ shlex .quote (session .bin )}  "{ os .pathsep }  $PATH" 
106+             """ ,
107+     }
108+ 
109+     hookdir  =  Path (".git" ) /  "hooks" 
110+     if  not  hookdir .is_dir ():
111+         return 
112+ 
113+     for  hook  in  hookdir .iterdir ():
114+         if  hook .name .endswith (".sample" ) or  not  hook .is_file ():
115+             continue 
116+ 
117+         if  not  hook .read_bytes ().startswith (b"#!" ):
118+             continue 
119+ 
120+         text  =  hook .read_text ()
121+ 
122+         if  not  any (
123+             Path ("A" ) ==  Path ("a" ) and  bindir .lower () in  text .lower () or  bindir  in  text 
124+             for  bindir  in  bindirs 
125+         ):
126+             continue 
127+ 
128+         lines  =  text .splitlines ()
129+ 
130+         for  executable , header  in  headers .items ():
131+             if  executable  in  lines [0 ].lower ():
132+                 lines .insert (1 , dedent (header ))
133+                 hook .write_text ("\n " .join (lines ))
134+                 break 
135+ 
136+ 
28137@session (name = "pre-commit" , python = python_versions [0 ]) 
29138def  precommit (session : Session ) ->  None :
30139    """Lint using pre-commit.""" 
140+     session_install_uv (session , install_dev = True )
31141    args  =  session .posargs  or  [
32142        "run" ,
33143        "--all-files" ,
34144        "--hook-stage=manual" ,
35145        "--show-diff-on-failure" ,
36146    ]
37-     session .run (
38-         "uv" ,
39-         "pip" ,
40-         "install" ,
41-         "black" ,
42-         "darglint" ,
43-         "flake8" ,
44-         "flake8-bandit" ,
45-         "flake8-bugbear" ,
46-         "flake8-docstrings" ,
47-         "flake8-rst-docstrings" ,
48-         "isort" ,
49-         "pep8-naming" ,
50-         "pre-commit" ,
51-         "pre-commit-hooks" ,
52-         "pyupgrade" ,
53-         external = True ,
54-     )
147+     session_install_uv_package (session , ["pre-commit" ])
55148    session .run ("pre-commit" , * args )
149+     if  args  and  args [0 ] ==  "install" :
150+         activate_virtualenv_in_precommit_hooks (session )
56151
57152
58153@session (python = python_versions [0 ]) 
59154def  safety (session : Session ) ->  None :
60155    """Scan dependencies for insecure packages.""" 
61156    with  NamedTemporaryFile (delete = False ) as  requirements :
62-         session .run (
63-             "uv" ,
64-             "pip" ,
65-             "compile" ,
66-             "pyproject.toml" ,
67-             "--output-file" ,
68-             requirements .name ,
69-             external = True ,
70-         )
71-         session .run ("uv" , "pip" , "install" , "safety" , external = True )
157+         session_install_uv (session , install_dev = True )
158+         session_install_uv_package (session , ["safety" ])
72159        # TODO(Altay): Remove the CVE ignore once its resolved. 
73160        # It's not critical, so ignoring now. 
74161        ignore  =  ["70612" ]
@@ -88,8 +175,8 @@ def safety(session: Session) -> None:
88175def  mypy (session : Session ) ->  None :
89176    """Type-check using mypy.""" 
90177    args  =  session .posargs  or  ["src" , "tests" , "docs/conf.py" ]
91-     session . run_always ( "uv" ,  "pip" ,  "install" ,  "-e" ,  "." ,  external = True )
92-     session . run ( "uv" ,  "pip" ,  "install" ,  " mypy" , "pytest" ,  external = True )
178+     session_install_uv ( session ,  install_dev = True )
179+     session_install_uv_package ( session , [ " mypy" , "pytest" ] )
93180    session .run ("mypy" , * args )
94181    if  not  session .posargs :
95182        session .run ("mypy" , f"--python-executable={ sys .executable }  " , "noxfile.py" )
@@ -98,7 +185,10 @@ def mypy(session: Session) -> None:
98185@session (python = python_versions ) 
99186def  tests (session : Session ) ->  None :
100187    """Run the test suite.""" 
101-     session .run_always ("uv" , "pip" , "install" , "-e" , ".[cloud]" , external = True )
188+     session_install_uv (session , install_project = True , install_dev = True )
189+     session_install_uv_package (
190+         session , ["coverage" , "pytest" , "pygments" , "pytest-dependency" , "s3fs" ]
191+     )
102192    session .run (
103193        "uv" ,
104194        "pip" ,
@@ -122,7 +212,8 @@ def coverage(session: Session) -> None:
122212    """Produce the coverage report.""" 
123213    args  =  session .posargs  or  ["report" ]
124214
125-     session .run ("uv" , "pip" , "install" , "coverage[toml]" , external = True )
215+     session_install_uv (session , install_project = True , install_dev = True )
216+     session_install_uv_package (session , ["coverage" ])
126217
127218    if  not  session .posargs  and  any (Path ().glob (".coverage.*" )):
128219        session .run ("coverage" , "combine" , external = True )
@@ -133,10 +224,8 @@ def coverage(session: Session) -> None:
133224@session (python = python_versions [0 ]) 
134225def  typeguard (session : Session ) ->  None :
135226    """Runtime type checking using Typeguard.""" 
136-     session .run_always ("uv" , "pip" , "install" , "-e" , "." , external = True )
137-     session .run (
138-         "uv" , "pip" , "install" , "pytest" , "typeguard" , "pygments" , external = True 
139-     )
227+     session_install_uv (session , install_project = True , install_dev = True )
228+     session_install_uv_package (session , ["pytest" , "typeguard" , "pygments" ])
140229    session .run ("pytest" , f"--typeguard-packages={ package }  " , * session .posargs )
141230
142231
@@ -150,8 +239,8 @@ def xdoctest(session: Session) -> None:
150239        if  "FORCE_COLOR"  in  os .environ :
151240            args .append ("--colored=1" )
152241
153-     session . run_always ( "uv" ,  "pip" ,  "install" ,  "-e" ,  "." ,  external = True )
154-     session . run ( "uv" ,  "pip" ,  "install" ,  " xdoctest[colors]" ,  external = True )
242+     session_install_uv ( session ,  install_project = True ,  install_dev = True )
243+     session_install_uv_package ( session , [ " xdoctest" ] )
155244    session .run ("python" , "-m" , "xdoctest" , * args )
156245
157246
@@ -162,18 +251,17 @@ def docs_build(session: Session) -> None:
162251    if  not  session .posargs  and  "FORCE_COLOR"  in  os .environ :
163252        args .insert (0 , "--color" )
164253
165-     session .run_always ("uv" , "pip" , "install" , "-e" , "." , external = True )
166-     session .run (
167-         "uv" ,
168-         "pip" ,
169-         "install" ,
170-         "sphinx" ,
171-         "sphinx-click" ,
172-         "sphinx-copybutton" ,
173-         "furo" ,
174-         "myst-nb" ,
175-         "linkify-it-py" ,
176-         external = True ,
254+     session_install_uv (session , install_project = True , install_dev = True )
255+     session_install_uv_package (
256+         session ,
257+         [
258+             "sphinx" ,
259+             "sphinx-click" ,
260+             "sphinx-copybutton" ,
261+             "furo" ,
262+             "myst-nb" ,
263+             "linkify-it-py" ,
264+         ],
177265    )
178266
179267    build_dir  =  Path ("docs" , "_build" )
@@ -187,19 +275,18 @@ def docs_build(session: Session) -> None:
187275def  docs (session : Session ) ->  None :
188276    """Build and serve the documentation with live reloading on file changes.""" 
189277    args  =  session .posargs  or  ["--open-browser" , "docs" , "docs/_build" ]
190-     session .run_always ("uv" , "pip" , "install" , "-e" , "." , external = True )
191-     session .run (
192-         "uv" ,
193-         "pip" ,
194-         "install" ,
195-         "sphinx" ,
196-         "sphinx-autobuild" ,
197-         "sphinx-click" ,
198-         "sphinx-copybutton" ,
199-         "furo" ,
200-         "myst-nb" ,
201-         "linkify-it-py" ,
202-         external = True ,
278+     session_install_uv (session , install_project = True , install_dev = True )
279+     session_install_uv_package (
280+         session ,
281+         [
282+             "sphinx" ,
283+             "sphinx-autobuild" ,
284+             "sphinx-click" ,
285+             "sphinx-copybutton" ,
286+             "furo" ,
287+             "myst-nb" ,
288+             "linkify-it-py" ,
289+         ],
203290    )
204291
205292    build_dir  =  Path ("docs" , "_build" )
0 commit comments