33from projspec .proj .base import ProjectSpec
44from projspec .utils import AttrDict
55
6- # UV also allows dependencies (and other metadata)
7- # to be declared inside scripts, which means you can have one-file projects.
8- # https://docs.astral.sh/uv/guides/scripts/#declaring-script-dependencies
9- # example:
10- # /// script
11- # # dependencies = [
12- # # "requests<3",
13- # # "rich",
14- # # ]
15- # # ///
16-
17-
18- class UVScript (ProjectSpec ):
19- """Single-file project runnable by UV as a script
20-
21- Metadata are declared inline in the script header
22- See https://docs.astral.sh/uv/guides/scripts/#declaring-script-dependencies
23-
24- Note that UV explicitly allows running these directly from HTTP URLs.
25- """
26-
27- spec_doc = "https://docs.astral.sh/uv/reference/settings/"
28-
29- def match (self ):
30- # this is a file, not a directory
31- return self .root .url .endswith (("py" , "pyw" ))
32-
33- def parse (self ):
34- try :
35- with self .root .fs .open (self .root .url ) as f :
36- txt = f .read ().decode ()
37- except OSError as e :
38- raise ValueError from e
39- lines = txt .split ("# /// script\n " , 1 )[1 ].txt .split ("# ///\n " , 1 )[0 ]
40- meta = "\n " .join (line [2 :] for line in lines .split ("\n " ))
41- toml .loads (meta )
42- # TODO: once we have the meta, we can reuse UVProject
43- #
44- # Apparently, uv.lock may or may not be in the same directory.
45-
466
47- class UV (ProjectSpec ):
48- """UV-runnable project
49-
50- Note: uv can run any python project, but this tests for uv-specific
51- config.
52- """
53-
54- def match (self ):
55- if not {"uv.lock" , "uv.toml" , ".python-version" }.isdisjoint (
56- self .root .basenames
57- ):
58- return True
59- if "uv" in self .root .pyproject .get ("tools" , {}):
60- return True
61- if (
62- self .root .pyproject .get ("build-system" , {}).get ("build-backend" , "" )
63- == "uv_build"
64- ):
65- return True
66- if ".venv" in self .root .basenames :
67- try :
68- with self .root .fs .open (
69- f"{ self .root .url } /.venv/pyvenv.cfg" , "rt"
70- ) as f :
71- txt = f .read ()
72- return b"uv =" in txt
73- except (OSError , FileNotFoundError ):
74- pass
75- return False
76-
77- def parse (self ):
7+ class UVMixin :
8+ def _parse_meta (self , conf ):
789 from projspec .artifact .installable import Wheel
7910 from projspec .artifact .python_env import LockFile , VirtualEnv
8011 from projspec .content .environment import Environment , Precision , Stack
8112
8213 meta = self .root .pyproject
83- conf = meta .get ("tools" , {}).get ("uv" , {})
84- try :
85- with self .root .fs .open (f"{ self .root .url } /uv.toml" , "rt" ) as f :
86- conf2 = toml .load (f )
87- except (OSError , FileNotFoundError ):
88- conf2 = {}
89- conf .update (conf2 )
90- try :
91- with self .root .fs .open (f"{ self .root .url } /uv.lock" , "rt" ) as f :
92- lock = toml .load (f )
93- except (OSError , FileNotFoundError ):
94- lock = {}
9514
9615 envs = AttrDict ()
9716 # TODO: uv allows dependencies with source=, which would show us where the
@@ -155,6 +74,84 @@ def parse(self):
15574 cmd = ["uv" , "build" ],
15675 )
15776
77+
78+ class UVScript (ProjectSpec , UVMixin ):
79+ """Single-file project runnable by UV as a script
80+
81+ Metadata are declared inline in the script header
82+ See https://docs.astral.sh/uv/guides/scripts/#declaring-script-dependencies
83+
84+ """
85+
86+ spec_doc = "https://docs.astral.sh/uv/reference/settings/"
87+
88+ def match (self ):
89+ # this is a file, not a directory
90+ return self .root .url .endswith (("py" , "pyw" ))
91+
92+ def parse (self ):
93+ try :
94+ with self .root .fs .open (self .root .url ) as f :
95+ txt = f .read ().decode ()
96+ except OSError as e :
97+ raise ValueError from e
98+ lines = txt .split ("# /// script\n " , 1 )[1 ].txt .split ("# ///\n " , 1 )[0 ]
99+ meta = "\n " .join (line [2 :] for line in lines .split ("\n " ))
100+ self ._parse_meta (toml .loads (meta ))
101+ # if URL is local or http(s):
102+ # self.artifacts["process"] = Process(
103+ # proj=self.root, cmd=['uv', 'run', self.root.url]
104+ # )
105+
106+
107+ class UV (ProjectSpec , UVMixin ):
108+ """UV-runnable project
109+
110+ Note: uv can run any python project, but this tests for uv-specific
111+ config.
112+ """
113+
114+ def match (self ):
115+ if not {"uv.lock" , "uv.toml" , ".python-version" }.isdisjoint (
116+ self .root .basenames
117+ ):
118+ return True
119+ if "uv" in self .root .pyproject .get ("tools" , {}):
120+ return True
121+ if (
122+ self .root .pyproject .get ("build-system" , {}).get ("build-backend" , "" )
123+ == "uv_build"
124+ ):
125+ return True
126+ if ".venv" in self .root .basenames :
127+ try :
128+ with self .root .fs .open (
129+ f"{ self .root .url } /.venv/pyvenv.cfg" , "rt"
130+ ) as f :
131+ txt = f .read ()
132+ return b"uv =" in txt
133+ except (OSError , FileNotFoundError ):
134+ pass
135+ return False
136+
137+ def parse (self ):
138+ from projspec .content .environment import Environment , Precision , Stack
139+
140+ meta = self .root .pyproject
141+ conf = meta .get ("tools" , {}).get ("uv" , {})
142+ try :
143+ with self .root .fs .open (f"{ self .root .url } /uv.toml" , "rt" ) as f :
144+ conf2 = toml .load (f )
145+ except (OSError , FileNotFoundError ):
146+ conf2 = {}
147+ conf .update (conf2 )
148+ try :
149+ with self .root .fs .open (f"{ self .root .url } /uv.lock" , "rt" ) as f :
150+ lock = toml .load (f )
151+ except (OSError , FileNotFoundError ):
152+ lock = {}
153+ self ._parse_meta (conf )
154+
158155 if lock :
159156 pkg = [f"python { lock ['requires-python' ]} " ]
160157 # TODO: check for source= packages as opposed to pip wheel installs
@@ -164,7 +161,7 @@ def parse(self):
164161 for _ in lock ["package" ]
165162 ]
166163 )
167- envs ["lockfile" ] = Environment (
164+ self . contents . environment ["lockfile" ] = Environment (
168165 proj = self .root ,
169166 stack = Stack .PIP ,
170167 precision = Precision .LOCK ,
0 commit comments