4545from urllib .request import urlopen
4646
4747from ..environment import Environment
48- from ..scheme import Scheme , Schemes
48+ from ..scheme import Scheme
49+ from .. import scheme
50+
51+
52+ # Type alias for progress callback
53+ ProgressConsumer = Callable [[str , int , int ], None ]
4954
5055
5156class BuildException (Exception ):
@@ -67,19 +72,15 @@ def __init__(
6772 message: Error message
6873 cause: The underlying exception that caused the build to fail
6974 """
70- self .builder = builder
71- if message is None and cause is not None :
75+ if message is None :
7276 noun = "build" if builder is None else f"{ builder .name ()} build"
7377 verb = "interrupted" if isinstance (cause , KeyboardInterrupt ) else "failed"
7478 message = f"{ noun } { verb } "
7579 super ().__init__ (message )
80+ self .builder = builder
7681 self .__cause__ = cause
7782
7883
79- # Type alias for progress callback
80- ProgressConsumer = Callable [[str , int , int ], None ]
81-
82-
8384class Builder (Protocol ):
8485 """
8586 Base protocol for all Appose environment builders.
@@ -555,9 +556,9 @@ def _env_dir(self) -> Path:
555556 def _scheme (self ) -> Scheme :
556557 """Get the scheme, detecting from content if needed."""
557558 if self .scheme :
558- return Schemes .from_name (self .scheme )
559+ return scheme .from_name (self .scheme )
559560 if self .source_content :
560- return Schemes .from_content (self .source_content )
561+ return scheme .from_content (self .source_content )
561562 raise ValueError ("Cannot determine scheme: neither scheme nor content is set" )
562563
563564 def _create_env (
@@ -615,7 +616,7 @@ class SimpleBuilder(BaseBuilder):
615616
616617 def __init__ (self ):
617618 super ().__init__ ()
618- self .custom_bin_paths : list [str ] = []
619+ self ._custom_bin_paths : list [str ] = []
619620
620621 def name (self ) -> str :
621622 return "custom"
@@ -631,7 +632,7 @@ def bin_paths(self, *paths: str) -> SimpleBuilder:
631632 Returns:
632633 This builder instance
633634 """
634- self .custom_bin_paths .extend (paths )
635+ self ._custom_bin_paths .extend (paths )
635636 return self
636637
637638 def append_system_path (self ) -> SimpleBuilder :
@@ -642,7 +643,7 @@ def append_system_path(self) -> SimpleBuilder:
642643 This builder instance
643644 """
644645 system_path = os .environ .get ("PATH" , "" ).split (os .pathsep )
645- self .custom_bin_paths .extend (system_path )
646+ self ._custom_bin_paths .extend (system_path )
646647 return self
647648
648649 def inherit_running_java (self ) -> SimpleBuilder :
@@ -659,7 +660,7 @@ def inherit_running_java(self) -> SimpleBuilder:
659660 java_home_bin = Path (java_home ) / "bin"
660661 if java_home_bin .is_dir ():
661662 # Prepend to beginning of list for highest priority
662- self .custom_bin_paths .insert (0 , str (java_home_bin ))
663+ self ._custom_bin_paths .insert (0 , str (java_home_bin ))
663664 self .env_vars_dict ["JAVA_HOME" ] = java_home
664665 return self
665666
@@ -688,7 +689,7 @@ def build(self) -> Environment:
688689 bin_paths .append (str (bin_dir .absolute ()))
689690
690691 # Add custom binary paths configured via builder methods
691- bin_paths .extend (self .custom_bin_paths )
692+ bin_paths .extend (self ._custom_bin_paths )
692693
693694 return self ._create_env (base_path , bin_paths , launch_args )
694695
@@ -718,40 +719,6 @@ def _env_dir(self) -> Path:
718719 return self .env_dir if self .env_dir else Path ("." )
719720
720721
721- class SimpleBuilderFactory :
722- """
723- Factory for creating SimpleBuilder instances.
724- SimpleBuilder can wrap any directory as a fallback.
725- """
726-
727- def create_builder (self ) -> Builder :
728- """Create a SimpleBuilder instance."""
729- return SimpleBuilder ()
730-
731- def name (self ) -> str :
732- """Return the name of the builder."""
733- return "custom"
734-
735- def supports_scheme (self , scheme : str ) -> bool :
736- """SimpleBuilder doesn't support any specific schemes."""
737- return False
738-
739- def priority (self ) -> float :
740- """
741- Lowest priority - SimpleBuilder is a fallback.
742- Always tried last.
743- """
744- return 0.0
745-
746- def can_wrap (self , env_dir : str | Path ) -> bool :
747- """
748- SimpleBuilder can wrap any existing directory as a fallback.
749- This ensures that any directory can be used as an environment.
750- """
751- env_path = Path (env_dir ) if isinstance (env_dir , str ) else env_dir
752- return env_path .exists () and env_path .is_dir ()
753-
754-
755722class DynamicBuilder (BaseBuilder ):
756723 """
757724 Dynamic builder that auto-detects the appropriate specific builder
@@ -838,93 +805,7 @@ def _create_builder(
838805 raise ValueError ("Content and/or scheme must be provided for dynamic builder" )
839806
840807
841- # Builders utility class
842-
843-
844- class Builders :
845- """
846- Utility class for discovering and managing environment builder factories.
847-
848- Uses entry points for plugin discovery instead of Java's ServiceLoader.
849- """
850-
851- _ALL_FACTORIES : list [BuilderFactory ] | None = None
852-
853- @staticmethod
854- def _discover_factories () -> list [BuilderFactory ]:
855- """
856- Discover all BuilderFactory implementations via entry points.
857-
858- Returns:
859- List of factories sorted by priority (highest first)
860- """
861- if Builders ._ALL_FACTORIES is not None :
862- return Builders ._ALL_FACTORIES
863-
864- factories : list [BuilderFactory ] = []
865-
866- # Try modern importlib.metadata (Python 3.8+)
867- try :
868- from importlib .metadata import entry_points
869- except ImportError :
870- # Fall back to importlib_metadata for older Python
871- try :
872- from importlib_metadata import entry_points
873- except ImportError :
874- # If no entry point support, use hardcoded defaults
875- from .pixi import PixiBuilderFactory
876- from .mamba import MambaBuilderFactory
877- from .uv import UvBuilderFactory
878-
879- Builders ._ALL_FACTORIES = sorted (
880- [
881- PixiBuilderFactory (),
882- MambaBuilderFactory (),
883- UvBuilderFactory (),
884- SimpleBuilderFactory (),
885- ],
886- key = lambda f : f .priority (),
887- reverse = True ,
888- )
889- return Builders ._ALL_FACTORIES
890-
891- # Load from entry points
892- eps = entry_points ()
893- # Handle both old and new entry_points() API
894- if hasattr (eps , "select" ):
895- # New API (Python 3.10+)
896- builder_eps = eps .select (group = "appose.builders" )
897- else :
898- # Old API (Python 3.8-3.9)
899- builder_eps = eps .get ("appose.builders" , [])
900-
901- for ep in builder_eps :
902- try :
903- factory_class = ep .load ()
904- factories .append (factory_class ())
905- except Exception as e :
906- print (
907- f"Warning: Failed to load builder factory { ep .name } : { e } " ,
908- file = sys .stderr ,
909- )
910-
911- # If no entry points found, fall back to hardcoded defaults
912- if not factories :
913- from .pixi import PixiBuilderFactory
914- from .mamba import MambaBuilderFactory
915- from .uv import UvBuilderFactory
916-
917- factories = [
918- PixiBuilderFactory (),
919- MambaBuilderFactory (),
920- UvBuilderFactory (),
921- SimpleBuilderFactory (),
922- ]
923-
924- # Sort by priority (highest first)
925- factories .sort (key = lambda f : f .priority (), reverse = True )
926- Builders ._ALL_FACTORIES = factories
927- return factories
808+ _BUILDERS : list [BuilderFactory ] | None = None
928809
929810
930811def find_factory_by_name (name : str ) -> BuilderFactory | None :
@@ -937,7 +818,7 @@ def find_factory_by_name(name: str) -> BuilderFactory | None:
937818 Returns:
938819 The factory with matching name, or None if not found
939820 """
940- factories = Builders . _discover_factories ()
821+ factories = _discover_factories ()
941822 for factory in factories :
942823 if factory .name ().lower () == name .lower ():
943824 return factory
@@ -955,7 +836,7 @@ def find_factory_by_scheme(scheme: str) -> BuilderFactory | None:
955836 Returns:
956837 The first factory that supports the scheme, or None if none found
957838 """
958- factories = Builders . _discover_factories ()
839+ factories = _discover_factories ()
959840 for factory in factories :
960841 if factory .supports_scheme (scheme ):
961842 return factory
@@ -973,7 +854,7 @@ def find_factory_for_wrapping(env_dir: str | Path) -> BuilderFactory | None:
973854 Returns:
974855 The first factory that can wrap the directory, or None if none found
975856 """
976- factories = Builders . _discover_factories ()
857+ factories = _discover_factories ()
977858 for factory in factories :
978859 if factory .can_wrap (env_dir ):
979860 return factory
@@ -1005,3 +886,78 @@ def env_type(env_dir: str | Path) -> str | None:
1005886 """
1006887 factory = find_factory_for_wrapping (env_dir )
1007888 return factory .name () if factory else None
889+
890+
891+ def _discover_factories () -> list [BuilderFactory ]:
892+ """
893+ Discover all BuilderFactory implementations via entry points.
894+
895+ Returns:
896+ List of factories sorted by priority (highest first)
897+ """
898+ global _BUILDERS
899+ if _BUILDERS is not None :
900+ return _BUILDERS
901+
902+ factories : list [BuilderFactory ] = []
903+
904+ # Try modern importlib.metadata (Python 3.8+)
905+ try :
906+ from importlib .metadata import entry_points
907+ except ImportError :
908+ # Fall back to importlib_metadata for older Python
909+ try :
910+ from importlib_metadata import entry_points
911+ except ImportError :
912+ # If no entry point support, use hardcoded defaults
913+ from .pixi import PixiBuilderFactory
914+ from .mamba import MambaBuilderFactory
915+ from .uv import UvBuilderFactory
916+
917+ _BUILDERS = sorted (
918+ [
919+ PixiBuilderFactory (),
920+ MambaBuilderFactory (),
921+ UvBuilderFactory (),
922+ ],
923+ key = lambda f : f .priority (),
924+ reverse = True ,
925+ )
926+ return _BUILDERS
927+
928+ # Load from entry points
929+ eps = entry_points ()
930+ # Handle both old and new entry_points() API
931+ if hasattr (eps , "select" ):
932+ # New API (Python 3.10+)
933+ builder_eps = eps .select (group = "appose.builders" )
934+ else :
935+ # Old API (Python 3.8-3.9)
936+ builder_eps = eps .get ("appose.builders" , [])
937+
938+ for ep in builder_eps :
939+ try :
940+ factory_class = ep .load ()
941+ factories .append (factory_class ())
942+ except Exception as e :
943+ print (
944+ f"Warning: Failed to load builder factory { ep .name } : { e } " ,
945+ file = sys .stderr ,
946+ )
947+
948+ # If no entry points found, fall back to hardcoded defaults
949+ if not factories :
950+ from .pixi import PixiBuilderFactory
951+ from .mamba import MambaBuilderFactory
952+ from .uv import UvBuilderFactory
953+
954+ factories = [
955+ PixiBuilderFactory (),
956+ MambaBuilderFactory (),
957+ UvBuilderFactory (),
958+ ]
959+
960+ # Sort by priority (highest first)
961+ factories .sort (key = lambda f : f .priority (), reverse = True )
962+ _BUILDERS = factories
963+ return factories
0 commit comments