Skip to content

Commit 43aed33

Browse files
ctruedenclaude
andcommitted
Improve Appose entry points
This significantly simplifies the builder subsystem. We don't need the concept of source strings in the builders themselves, only in the toplevel appose convenience entry methods. Appose now provides four builders, with three options per builder: Builder | Plain | Auto | Path | URL --------|---------|------------|-----------------------|---- Pixi | pixi() | pixi(str) | pixi(Path) | <none> Mamba | mamba() | mamba(str) | mamba(Path) | <none> Uv | uv() | uv(str) | uv(Path) | <none> Dynamic | <none> | <none> | file(str), file(Path) | url(str) As per apposed/appose-java@427e568. Co-authored-by: Claude <[email protected]>
1 parent 52cda5d commit 43aed33

File tree

9 files changed

+198
-171
lines changed

9 files changed

+198
-171
lines changed

src/appose/__init__.py

Lines changed: 160 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -214,32 +214,166 @@ def task_listener(event):
214214
}
215215
'''
216216

217+
from __future__ import annotations
218+
219+
import re
217220
from pathlib import Path
218221

219-
from .builder import SimpleBuilder
222+
from .builder import (
223+
BuildException,
224+
DynamicBuilder,
225+
SimpleBuilder,
226+
find_factory_for_wrapping,
227+
)
228+
from .builder.pixi import PixiBuilder
229+
from .builder.mamba import MambaBuilder
230+
from .builder.uv import UvBuilder
220231
from .environment import Environment
221232
from .service import TaskException # noqa: F401
222233
from .shm import NDArray, SharedMemory # noqa: F401
223234

224235
__version__ = "0.7.3.dev0"
225236

226237

227-
def base(directory: Path) -> SimpleBuilder:
238+
def pixi(source: str | Path | None = None) -> PixiBuilder:
239+
"""
240+
Creates a PixiBuilder for Pixi-based environments.
241+
242+
Args:
243+
source: Optional configuration source (file path or URL)
244+
245+
Returns:
246+
A PixiBuilder instance
247+
"""
248+
builder = PixiBuilder()
249+
if source:
250+
if isinstance(source, Path) or not _is_url(str(source)):
251+
return builder.file(source)
252+
else:
253+
return builder.url(str(source))
254+
return builder
255+
256+
257+
def mamba(source: str | Path | None = None) -> MambaBuilder:
258+
"""
259+
Creates a MambaBuilder for Micromamba-based environments.
260+
261+
Args:
262+
source: Optional configuration source (file path or URL)
263+
264+
Returns:
265+
A MambaBuilder instance
266+
"""
267+
builder = MambaBuilder()
268+
if source:
269+
if isinstance(source, Path) or not _is_url(str(source)):
270+
return builder.file(source)
271+
else:
272+
return builder.url(str(source))
273+
return builder
274+
275+
276+
def uv(source: str | Path | None = None) -> UvBuilder:
228277
"""
229-
Create a simple builder with a custom base directory.
278+
Creates a UvBuilder for uv-based virtual environments.
230279
231280
Args:
232-
directory: The base directory for the environment
281+
source: Optional configuration source (file path or URL)
233282
234283
Returns:
235-
A SimpleBuilder instance configured with the given directory
284+
A UvBuilder instance
236285
"""
237-
return SimpleBuilder().base(directory)
286+
builder = UvBuilder()
287+
if source:
288+
if isinstance(source, Path) or not _is_url(str(source)):
289+
return builder.file(source)
290+
else:
291+
return builder.url(str(source))
292+
return builder
238293

239294

240-
def system(directory: Path = Path(".")) -> Environment:
295+
def file(source: str | Path) -> DynamicBuilder:
241296
"""
242-
Create a simple environment using system executables.
297+
Creates a DynamicBuilder from a configuration file.
298+
The builder type will be auto-detected from file content.
299+
300+
Args:
301+
source: Path to configuration file
302+
303+
Returns:
304+
A DynamicBuilder instance
305+
"""
306+
return DynamicBuilder().file(source)
307+
308+
309+
def url(source: str) -> DynamicBuilder:
310+
"""
311+
Creates a DynamicBuilder from a URL.
312+
The builder type will be auto-detected from content.
313+
314+
Args:
315+
source: URL to configuration file
316+
317+
Returns:
318+
A DynamicBuilder instance
319+
"""
320+
return DynamicBuilder().url(source)
321+
322+
323+
def content(config_content: str) -> DynamicBuilder:
324+
"""
325+
Creates a DynamicBuilder from configuration content.
326+
The builder type will be auto-detected from content syntax.
327+
328+
Args:
329+
config_content: Configuration file content
330+
331+
Returns:
332+
A DynamicBuilder instance
333+
"""
334+
return DynamicBuilder().content(config_content)
335+
336+
337+
def wrap(env_dir: str | Path) -> Environment:
338+
"""
339+
Wraps an existing environment directory, auto-detecting its type.
340+
341+
Args:
342+
env_dir: The directory containing the environment
343+
344+
Returns:
345+
An Environment configured for the detected type
346+
347+
Raises:
348+
BuildException: If the directory doesn't exist
349+
"""
350+
env_path = Path(env_dir)
351+
if not env_path.exists():
352+
raise BuildException(None, f"Environment directory does not exist: {env_dir}")
353+
354+
# Find a builder factory that can wrap this directory
355+
factory = find_factory_for_wrapping(env_path)
356+
357+
if factory:
358+
return factory.create_builder().wrap(env_path)
359+
360+
# Default to simple builder (no special activation)
361+
return custom().wrap(env_path)
362+
363+
364+
def custom() -> SimpleBuilder:
365+
"""
366+
Creates a SimpleBuilder for custom environments without package management.
367+
368+
Returns:
369+
A SimpleBuilder instance
370+
"""
371+
return SimpleBuilder()
372+
373+
374+
def system(directory: str | Path = Path(".")) -> Environment:
375+
"""
376+
Creates a simple environment using system executables.
243377
244378
Args:
245379
directory: The working directory (defaults to current directory)
@@ -248,3 +382,21 @@ def system(directory: Path = Path(".")) -> Environment:
248382
An Environment that uses system PATH for finding executables
249383
"""
250384
return SimpleBuilder().base(directory).append_system_path().build()
385+
386+
387+
def _is_url(source: str) -> bool:
388+
"""
389+
Check if string appears to be a URL.
390+
Detects common URL schemes (http, https, ftp, file, jar) by using
391+
a pattern of 3+ letter scheme followed by "://" to avoid matching
392+
Windows drive letters like "C:".
393+
394+
Args:
395+
source: The source string to check
396+
397+
Returns:
398+
True if the string looks like a URL
399+
"""
400+
if not source:
401+
return False
402+
return bool(re.match(r"^[a-z]{3,}://", source.lower()))

src/appose/builder/__init__.py

Lines changed: 16 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -357,21 +357,13 @@ class BuilderFactory(Protocol):
357357
the Builders utility class.
358358
"""
359359

360-
def create_builder(
361-
self, source: str | None = None, scheme: str | None = None
362-
) -> Builder:
360+
def create_builder(self) -> Builder:
363361
"""
364-
Creates a new builder instance.
365-
366-
Args:
367-
source: Optional source file path
368-
scheme: Optional scheme (e.g., "environment.yml", "pixi.toml")
362+
Creates a new builder instance with no configuration.
363+
Configuration should be provided via the fluent API (e.g., content, scheme).
369364
370365
Returns:
371366
A new builder instance
372-
373-
Raises:
374-
BuildException: If the builder cannot be created
375367
"""
376368
...
377369

@@ -396,19 +388,6 @@ def supports_scheme(self, scheme: str) -> bool:
396388
"""
397389
...
398390

399-
def supports_source(self, source: str) -> bool:
400-
"""
401-
Checks if this builder can build from the given source file.
402-
This allows builders to inspect file extensions or content to determine compatibility.
403-
404-
Args:
405-
source: The source file path to check
406-
407-
Returns:
408-
True if this builder can build from the source
409-
"""
410-
...
411-
412391
def priority(self) -> float:
413392
"""
414393
Returns the priority of this builder for scheme resolution.
@@ -577,7 +556,9 @@ def _scheme(self) -> Scheme:
577556
"""Get the scheme, detecting from content if needed."""
578557
if self.scheme:
579558
return Schemes.from_name(self.scheme)
580-
return Schemes.from_content(self.source_content)
559+
if self.source_content:
560+
return Schemes.from_content(self.source_content)
561+
raise ValueError("Cannot determine scheme: neither scheme nor content is set")
581562

582563
def _create_env(
583564
self, base: str, bin_paths: list[str], launch_args: list[str]
@@ -743,16 +724,9 @@ class SimpleBuilderFactory:
743724
SimpleBuilder can wrap any directory as a fallback.
744725
"""
745726

746-
def create_builder(
747-
self, source: str | None = None, scheme: str | None = None
748-
) -> Builder:
727+
def create_builder(self) -> Builder:
749728
"""Create a SimpleBuilder instance."""
750-
builder = SimpleBuilder()
751-
if source:
752-
builder.file(source)
753-
if scheme:
754-
builder.scheme(scheme)
755-
return builder
729+
return SimpleBuilder()
756730

757731
def name(self) -> str:
758732
"""Return the name of the builder."""
@@ -762,10 +736,6 @@ def supports_scheme(self, scheme: str) -> bool:
762736
"""SimpleBuilder doesn't support any specific schemes."""
763737
return False
764738

765-
def supports_source(self, source: str) -> bool:
766-
"""SimpleBuilder doesn't support any specific source files."""
767-
return False
768-
769739
def priority(self) -> float:
770740
"""
771741
Lowest priority - SimpleBuilder is a fallback.
@@ -785,12 +755,11 @@ def can_wrap(self, env_dir: str | Path) -> bool:
785755
class DynamicBuilder(BaseBuilder):
786756
"""
787757
Dynamic builder that auto-detects the appropriate specific builder
788-
based on source file and scheme.
758+
based on configuration content and scheme.
789759
"""
790760

791-
def __init__(self, source: str | None = None):
761+
def __init__(self):
792762
super().__init__()
793-
self.source = source
794763
self.builder_name: str | None = None
795764

796765
def builder(self, builder_name: str) -> DynamicBuilder:
@@ -811,13 +780,13 @@ def name(self) -> str:
811780

812781
def build(self) -> Environment:
813782
"""Build by delegating to the appropriate builder."""
814-
delegate = self._create_builder(self.builder_name, self.source, self.scheme)
783+
delegate = self._create_builder(self.builder_name, self.scheme)
815784
self._copy_config_to_delegate(delegate)
816785
return delegate.build()
817786

818787
def rebuild(self) -> Environment:
819788
"""Rebuild by delegating to the appropriate builder."""
820-
delegate = self._create_builder(self.builder_name, self.source, self.scheme)
789+
delegate = self._create_builder(self.builder_name, self.scheme)
821790
self._copy_config_to_delegate(delegate)
822791
return delegate.rebuild()
823792

@@ -844,16 +813,15 @@ def _copy_config_to_delegate(self, delegate: Builder) -> None:
844813
def _create_builder(
845814
self,
846815
name: str | None,
847-
source: str | None,
848816
scheme: str | None,
849817
) -> Builder:
850-
"""Create the appropriate builder based on name, source, and scheme."""
818+
"""Create the appropriate builder based on name and scheme."""
851819
# Find the builder matching the specified name, if any
852820
if name:
853821
factory = find_factory_by_name(name)
854822
if factory is None:
855823
raise ValueError(f"Unknown builder: {name}")
856-
return factory.create_builder(source, scheme)
824+
return factory.create_builder()
857825

858826
# Detect scheme from content if content is provided but scheme is not
859827
effective_scheme = scheme
@@ -865,18 +833,9 @@ def _create_builder(
865833
factory = find_factory_by_scheme(effective_scheme)
866834
if factory is None:
867835
raise ValueError(f"No builder supports scheme: {effective_scheme}")
868-
return factory.create_builder(source, effective_scheme)
836+
return factory.create_builder()
869837

870-
# Find the highest-priority builder that supports this source
871-
if source:
872-
factory = find_factory_by_source(source)
873-
if factory is None:
874-
raise ValueError(f"No builder supports source: {source}")
875-
return factory.create_builder(source)
876-
877-
raise ValueError(
878-
"At least one of builder name, source, content, and scheme must be non-null"
879-
)
838+
raise ValueError("Content and/or scheme must be provided for dynamic builder")
880839

881840

882841
# Builders utility class
@@ -1021,26 +980,6 @@ def find_factory_for_wrapping(env_dir: str | Path) -> BuilderFactory | None:
1021980
return None
1022981

1023982

1024-
def find_factory_by_source(source: str) -> BuilderFactory | None:
1025-
"""
1026-
Finds the first factory that can build from the given source file.
1027-
Factories are checked in priority order (highest priority first).
1028-
1029-
Args:
1030-
source: The source file path to find a factory for
1031-
1032-
Returns:
1033-
The first factory that can build from the source, or None if none found
1034-
"""
1035-
if source is None:
1036-
raise ValueError("Cannot auto-detect builder: no source specified")
1037-
factories = Builders._discover_factories()
1038-
for factory in factories:
1039-
if factory.supports_source(source):
1040-
return factory
1041-
return None
1042-
1043-
1044983
def can_wrap(env_dir: str | Path) -> bool:
1045984
"""
1046985
Checks if the given directory can be wrapped as a known environment type.

0 commit comments

Comments
 (0)