Skip to content

Commit e53c56b

Browse files
committed
Add support for passing extra parameters to package type handlers
Signed-off-by: Matthew Ballance <matt.ballance@gmail.com>
1 parent f7efcde commit e53c56b

File tree

15 files changed

+1078
-21
lines changed

15 files changed

+1078
-21
lines changed

docs/source/package_types.rst

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,49 @@ Python packages are installed into the project's virtual environment.
366366
- name: requests
367367
src: pypi # Automatically type: python
368368
369+
**Type-specific parameters (** ``with:`` **):**
370+
371+
When ``type: python`` is specified you can pass additional parameters under a
372+
``with:`` key to control how the package is installed:
373+
374+
``editable`` *(boolean, default: true)*
375+
Install the source package in editable mode (``pip install -e``).
376+
Set to ``false`` to install as a regular (non-editable) package.
377+
378+
``extras`` *(string or list of strings)*
379+
`PEP 508 extras <https://peps.python.org/pep-0508/#extras>`_ to request
380+
when installing (e.g. ``[tests]``, ``[litellm, openai]``).
381+
382+
.. code-block:: yaml
383+
384+
# Non-editable install
385+
- name: my-lib
386+
url: https://github.com/org/my-lib.git
387+
type: python
388+
with:
389+
editable: false
390+
391+
# Source package with extras
392+
- name: my-lib
393+
url: https://github.com/org/my-lib.git
394+
type: python
395+
with:
396+
extras: [tests, docs]
397+
398+
# Non-editable with extras
399+
- name: my-lib
400+
url: https://github.com/org/my-lib.git
401+
type: python
402+
with:
403+
extras: [tests]
404+
editable: false
405+
406+
.. note::
407+
408+
``with:`` is only valid when ``type:`` is also specified on the same entry.
409+
Unknown keys inside ``with:`` are a validation error.
410+
Parameter validation happens at YAML-read time, before any packages are fetched.
411+
369412
Raw (``raw``)
370413
-------------
371414

@@ -393,6 +436,12 @@ Raw packages are placed in ``packages/<name>/`` but not processed further.
393436
url: https://example.com/vectors.tar.gz
394437
type: raw
395438
439+
.. note::
440+
441+
``type: raw`` accepts no ``with:`` parameters. This type is also useful to
442+
suppress Python auto-detection on a package that happens to contain a
443+
``pyproject.toml`` but should not be pip-installed.
444+
396445
Auto-Detection
397446
==============
398447

docs/source/python_packages.rst

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,78 @@ Binary Packages
183183
184184
import requests # Uses installed package
185185
186+
Controlling Installation with ``with:``
187+
========================================
188+
189+
When ``type: python`` is declared on a package entry, an optional ``with:``
190+
block lets you pass type-specific installation parameters.
191+
192+
``editable`` — Non-editable Source Installs
193+
--------------------------------------------
194+
195+
By default, source packages (git, dir) are installed in *editable* mode
196+
(``pip install -e``). Set ``editable: false`` to install them as regular,
197+
non-editable packages instead. This is useful for dependencies that are
198+
stable releases you don't intend to modify.
199+
200+
.. code-block:: yaml
201+
202+
deps:
203+
# Default: editable install
204+
- name: my-lib
205+
url: https://github.com/org/my-lib.git
206+
type: python
207+
208+
# Non-editable install
209+
- name: stable-lib
210+
url: https://github.com/org/stable-lib.git
211+
type: python
212+
with:
213+
editable: false
214+
215+
``extras`` — PEP 508 Extras
216+
-----------------------------
217+
218+
Use ``extras`` to request `PEP 508 optional dependency groups
219+
<https://peps.python.org/pep-0508/#extras>`_ when installing a source package.
220+
The value may be a single string or a list.
221+
222+
.. code-block:: yaml
223+
224+
deps:
225+
# Single extra
226+
- name: my-lib
227+
url: https://github.com/org/my-lib.git
228+
type: python
229+
with:
230+
extras: tests
231+
232+
# Multiple extras
233+
- name: my-lib
234+
url: https://github.com/org/my-lib.git
235+
type: python
236+
with:
237+
extras: [tests, docs]
238+
239+
# Non-editable with extras
240+
- name: my-lib
241+
url: https://github.com/org/my-lib.git
242+
type: python
243+
with:
244+
extras: [tests]
245+
editable: false
246+
247+
.. note::
248+
249+
``with:`` is only valid when ``type:`` is also specified on the same entry.
250+
Unknown keys inside ``with:`` are reported as errors at YAML-read time,
251+
before any packages are fetched.
252+
253+
For PyPI packages (``src: pypi``), extras can also be specified using the
254+
top-level ``extras:`` field directly on the package entry — that form is
255+
preserved for backward compatibility and is the concise form for the common
256+
case.
257+
186258
PyPI Package Configuration
187259
==========================
188260

src/ivpm/handlers/package_handler_python.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from typing import Dict, List, Set
3030
from ..project_ops_info import ProjectUpdateInfo, ProjectBuildInfo
3131
from ..utils import note, fatal, get_venv_python
32+
from ..pkg_content_type import PythonTypeData
3233

3334
from ..package import Package
3435
from .package_handler import PackageHandler
@@ -49,6 +50,10 @@ def process_pkg(self, pkg: Package):
4950
if pkg.src_type == "pypi":
5051
self.pypi_pkg_s.add(pkg.name)
5152
add = True
53+
elif isinstance(pkg.type_data, PythonTypeData):
54+
# Explicit type: python via 'with:' mechanism
55+
self.src_pkg_s.add(pkg.name)
56+
add = True
5257
elif pkg.pkg_type is not None and pkg.pkg_type == PackageHandlerPython.name:
5358
self.src_pkg_s.add(pkg.name)
5459
add = True
@@ -408,17 +413,30 @@ def _write_requirements_txt(self,
408413
for pkg in python_pkgs:
409414

410415
if hasattr(pkg, "url"):
411-
# Editable package
412-
# fp.write("-e file://%s/%s#egg=%s\n" % (
413-
# packages_dir.replace("\\","/"),
414-
# pkg.name,
415-
# pkg.name))
416-
fp.write("-e %s/%s\n" % (
417-
packages_dir.replace("\\","/"),
418-
pkg.name))
416+
# Source package (git, dir, http, etc.)
417+
# Determine editability: type_data takes priority, then default True
418+
editable = True
419+
if isinstance(pkg.type_data, PythonTypeData) and pkg.type_data.editable is not None:
420+
editable = pkg.type_data.editable
421+
422+
# Extras from type_data (for source packages declared with type: python + with: extras:)
423+
extras = None
424+
if isinstance(pkg.type_data, PythonTypeData):
425+
extras = pkg.type_data.extras
426+
extras_str = "[%s]" % ",".join(extras) if extras else ""
427+
428+
pkg_path = "%s/%s" % (packages_dir.replace("\\","/"), pkg.name)
429+
if editable:
430+
fp.write("-e %s%s\n" % (pkg_path, extras_str))
431+
else:
432+
fp.write("%s%s\n" % (pkg_path, extras_str))
419433
else:
420434
# PyPi package — build PEP 508 specifier: name[extras]version
421-
extras = getattr(pkg, "extras", None)
435+
# Extras: prefer type_data if present, fall back to pkg.extras (PackagePyPi)
436+
if isinstance(pkg.type_data, PythonTypeData) and pkg.type_data.extras is not None:
437+
extras = pkg.type_data.extras
438+
else:
439+
extras = getattr(pkg, "extras", None)
422440
extras_str = "[%s]" % ",".join(extras) if extras else ""
423441
if pkg.version is not None:
424442
if pkg.version[0] in ['<','>','=']:

src/ivpm/ivpm_yaml_reader.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ def read_dep_sets(self, info : 'ProjInfo', dep_sets):
119119

120120
def read_deps(self, ret : PackagesInfo, deps, default_dep_set):
121121
from .pkg_types.pkg_type_rgy import PkgTypeRgy
122+
from .pkg_content_type_rgy import PkgContentTypeRgy
122123

123124
for d in deps:
124125
si = d.srcinfo
@@ -167,6 +168,20 @@ def read_deps(self, ret : PackagesInfo, deps, default_dep_set):
167168
raise Exception("Package %s has unknown type %s" % (d["name"], src))
168169
pkg = PkgTypeRgy.inst().mkPackage(src, str(d["name"]), d, si)
169170

171+
# Resolve content type and validate 'with:' parameters
172+
ct_rgy = PkgContentTypeRgy.inst()
173+
if "type" in d.keys():
174+
type_name = str(d["type"])
175+
if not ct_rgy.has(type_name):
176+
fatal("Package '%s': unknown type '%s' @ %s ; known types: %s" % (
177+
pkg.name, type_name, getlocstr(d["type"]),
178+
", ".join(ct_rgy.names())))
179+
with_opts = d["with"] if "with" in d.keys() else {}
180+
pkg.type_data = ct_rgy.get(type_name).create_data(with_opts, si)
181+
elif "with" in d.keys():
182+
fatal("Package '%s': 'with:' is specified but 'type:' is not @ %s" % (
183+
pkg.name, getlocstr(d["with"])))
184+
170185
# Unless specified, load the same dep-set from sub-packages
171186
if pkg.dep_set is None:
172187
if default_dep_set is not None:

src/ivpm/package.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import os
2525
import dataclasses as dc
2626
from enum import Enum, auto
27-
from typing import Dict, List, Set
27+
from typing import Dict, List, Set, Optional
2828
from .project_ops_info import ProjectUpdateInfo
2929
from .utils import fatal, getlocstr
3030

@@ -97,6 +97,9 @@ class Package(object):
9797
path : str = None
9898
pkg_type : PackageType = None
9999
src_type : str = None
100+
# type_data holds validated, type-specific parameters from the 'with:' YAML key.
101+
# Set by IvpmYamlReader when 'type:' is present; None when type is auto-detected.
102+
type_data : Optional['TypeData'] = None
100103

101104
process_deps : bool = True
102105
setup_deps : Set[str] = dc.field(default_factory=set)
@@ -148,14 +151,16 @@ def process_options(self, opts, si):
148151
else:
149152
fatal("Unknown value for 'deps': %s" % opts["deps"])
150153

151-
# Determine the package type (eg Python, Raw)
154+
# Set pkg_type from 'type:' for backward compatibility.
155+
# The authoritative typed data is stored in type_data by IvpmYamlReader
156+
# after calling PkgContentTypeRgy. We still set pkg_type here so that
157+
# code that has not yet migrated to type_data continues to work.
152158
if "type" in opts.keys():
153159
type_s = opts["type"]
154-
if not type_s in Spec2PackageType.keys():
155-
fatal("unknown package type %s @ %s ; Supported types 'raw', 'python'" % (
156-
type_s, getlocstr(opts["type"])))
157-
158-
self.pkg_type = Spec2PackageType[type_s]
160+
if type_s in Spec2PackageType.keys():
161+
self.pkg_type = Spec2PackageType[type_s]
162+
# Unknown type strings are tolerated here; the YAML reader validates them
163+
# via PkgContentTypeRgy and emits a proper error with source location.
159164

160165
@staticmethod
161166
def mk(name, opts, si) -> 'Package':

0 commit comments

Comments
 (0)