Skip to content

Commit b3ffe62

Browse files
committed
uv-scripts
1 parent a70a008 commit b3ffe62

File tree

1 file changed

+81
-84
lines changed

1 file changed

+81
-84
lines changed

src/projspec/proj/uv.py

Lines changed: 81 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -3,95 +3,14 @@
33
from projspec.proj.base import ProjectSpec
44
from 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

Comments
 (0)