@@ -48,9 +48,6 @@ def __init__(
4848 self .plugin_manager = plugin_manager
4949 self ._config = config
5050
51- self ._core_raw_metadata : dict [str , Any ] | None = None
52- self ._dynamic : list [str ] | None = None
53- self ._name : str | None = None
5451 self ._version : str | None = None
5552 self ._project_file : str | None = None
5653
@@ -70,68 +67,62 @@ def context(self) -> Context:
7067
7168 return Context (self .root )
7269
73- @property
70+ @cached_property
7471 def core_raw_metadata (self ) -> dict [str , Any ]:
75- if self ._core_raw_metadata is None :
76- if 'project' not in self .config :
77- message = 'Missing `project` metadata table in configuration'
78- raise ValueError (message )
79-
80- core_raw_metadata = self .config ['project' ]
81- if not isinstance (core_raw_metadata , dict ):
82- message = 'The `project` configuration must be a table'
83- raise TypeError (message )
72+ if 'project' not in self .config :
73+ message = 'Missing `project` metadata table in configuration'
74+ raise ValueError (message )
8475
85- core_raw_metadata = deepcopy ( core_raw_metadata )
86- pkg_info = os . path . join ( self . root , 'PKG-INFO' )
87- if os . path . isfile ( pkg_info ):
88- from hatchling . metadata . spec import PROJECT_CORE_METADATA_FIELDS , project_metadata_from_core_metadata
76+ core_raw_metadata = self . config [ 'project' ]
77+ if not isinstance ( core_raw_metadata , dict ):
78+ message = 'The `project` configuration must be a table'
79+ raise TypeError ( message )
8980
90- with open (pkg_info , encoding = 'utf-8' ) as f :
91- pkg_info_contents = f .read ()
81+ core_raw_metadata = deepcopy (core_raw_metadata )
82+ pkg_info = os .path .join (self .root , 'PKG-INFO' )
83+ if os .path .isfile (pkg_info ):
84+ from hatchling .metadata .spec import PROJECT_CORE_METADATA_FIELDS , project_metadata_from_core_metadata
9285
93- base_metadata = project_metadata_from_core_metadata (pkg_info_contents )
94- defined_dynamic = core_raw_metadata .get ('dynamic' , [])
95- for field in list (defined_dynamic ):
96- if field in PROJECT_CORE_METADATA_FIELDS and field in base_metadata :
97- core_raw_metadata [field ] = base_metadata [field ]
98- defined_dynamic .remove (field )
86+ with open (pkg_info , encoding = 'utf-8' ) as f :
87+ pkg_info_contents = f .read ()
9988
100- self ._core_raw_metadata = core_raw_metadata
89+ base_metadata = project_metadata_from_core_metadata (pkg_info_contents )
90+ defined_dynamic = core_raw_metadata .get ('dynamic' , [])
91+ for field in list (defined_dynamic ):
92+ if field in PROJECT_CORE_METADATA_FIELDS and field in base_metadata :
93+ core_raw_metadata [field ] = base_metadata [field ]
94+ defined_dynamic .remove (field )
10195
102- return self . _core_raw_metadata
96+ return core_raw_metadata
10397
104- @property
98+ @cached_property
10599 def dynamic (self ) -> list [str ]:
106- # Keep track of the original dynamic fields before depopulation
107- if self ._dynamic is None :
108- dynamic = self .core_raw_metadata .get ('dynamic' , [])
109- if not isinstance (dynamic , list ):
110- message = 'Field `project.dynamic` must be an array'
111- raise TypeError (message )
112-
113- for i , field in enumerate (dynamic , 1 ):
114- if not isinstance (field , str ):
115- message = f'Field #{ i } of field `project.dynamic` must be a string'
116- raise TypeError (message )
100+ # Here we maintain a copy of the dynamic fields from `self.core raw metadata`.
101+ # This property should never be mutated. In contrast, the fields in
102+ # `self.core.dynamic` are depopulated on the first evaulation of `self.core`
103+ # or `self.version` as the actual values are computed.
104+ dynamic = self .core_raw_metadata .get ('dynamic' , [])
105+ if not isinstance (dynamic , list ):
106+ message = 'Field `project.dynamic` must be an array'
107+ raise TypeError (message )
117108
118- self ._dynamic = list (dynamic )
109+ for i , field in enumerate (dynamic , 1 ):
110+ if not isinstance (field , str ):
111+ message = f'Field #{ i } of field `project.dynamic` must be a string'
112+ raise TypeError (message )
119113
120- return self . _dynamic
114+ return list ( dynamic )
121115
122- @property
116+ @cached_property
123117 def name (self ) -> str :
124118 # Duplicate the name parsing here for situations where it's
125119 # needed but metadata plugins might not be available
126- if self ._name is None :
127- name = self .core_raw_metadata .get ('name' , '' )
128- if not name :
129- message = 'Missing required field `project.name`'
130- raise ValueError (message )
131-
132- self ._name = normalize_project_name (name )
120+ name = self .core_raw_metadata .get ('name' , '' )
121+ if not name :
122+ message = 'Missing required field `project.name`'
123+ raise ValueError (message )
133124
134- return self . _name
125+ return normalize_project_name ( name )
135126
136127 @property
137128 def version (self ) -> str :
0 commit comments