1
- """Pydantic models for vcspull configuration."""
1
+ """Pydantic schemas for vcspull configuration."""
2
2
3
3
from __future__ import annotations
4
4
5
+ import enum
5
6
import os
6
7
import pathlib
7
8
import typing as t
8
- from enum import Enum
9
- from pathlib import Path
10
- from typing import Any , Dict , List , Optional , Union
11
9
12
10
from pydantic import (
13
11
BaseModel ,
14
12
ConfigDict ,
15
- Field ,
16
- HttpUrl ,
13
+ RootModel ,
17
14
field_validator ,
18
- model_validator ,
19
15
)
20
16
21
- if t .TYPE_CHECKING :
22
- from libvcs ._internal .types import VCSLiteral
23
- from libvcs .sync .git import GitSyncRemoteDict
24
-
25
17
# Type aliases for better readability
26
- PathLike = Union [str , Path ]
18
+ PathLike = t . Union [str , pathlib . Path ]
27
19
ConfigName = str
28
20
SectionName = str
29
21
ShellCommand = str
30
22
31
23
32
- class VCSType (str , Enum ):
24
+ class VCSType (str , enum . Enum ):
33
25
"""Supported version control systems."""
34
26
35
27
GIT = "git"
@@ -42,8 +34,8 @@ class GitRemote(BaseModel):
42
34
43
35
name : str
44
36
url : str
45
- fetch : Optional [ str ] = None
46
- push : Optional [ str ] = None
37
+ fetch : str | None = None
38
+ push : str | None = None
47
39
48
40
49
41
class RepositoryModel (BaseModel ):
@@ -67,10 +59,10 @@ class RepositoryModel(BaseModel):
67
59
68
60
vcs : str
69
61
name : str
70
- path : Union [ str , Path ]
62
+ path : str | pathlib . Path
71
63
url : str
72
- remotes : Optional [ Dict [ str , GitRemote ]] = None
73
- shell_command_after : Optional [ List [ str ]] = None
64
+ remotes : dict [ str , GitRemote ] | None = None
65
+ shell_command_after : list [ str ] | None = None
74
66
75
67
model_config = ConfigDict (
76
68
extra = "forbid" ,
@@ -97,15 +89,14 @@ def validate_vcs(cls, v: str) -> str:
97
89
ValueError
98
90
If VCS type is invalid
99
91
"""
100
- if v .lower () not in ("git" , "hg" , "svn" ):
101
- raise ValueError (
102
- f"Invalid VCS type: { v } . Supported types are: git, hg, svn"
103
- )
92
+ if v .lower () not in {"git" , "hg" , "svn" }:
93
+ msg = f"Invalid VCS type: { v } . Supported types are: git, hg, svn"
94
+ raise ValueError (msg )
104
95
return v .lower ()
105
96
106
97
@field_validator ("path" )
107
98
@classmethod
108
- def validate_path (cls , v : Union [ str , Path ] ) -> Path :
99
+ def validate_path (cls , v : str | pathlib . Path ) -> pathlib . Path :
109
100
"""Validate and convert path to Path object.
110
101
111
102
Parameters
@@ -127,12 +118,13 @@ def validate_path(cls, v: Union[str, Path]) -> Path:
127
118
# Convert to string first to handle Path objects
128
119
path_str = str (v )
129
120
# Expand environment variables and user directory
130
- expanded_path = os . path . expandvars (path_str )
131
- expanded_path = os .path . expanduser ( expanded_path )
132
- # Convert to Path object
133
- return Path ( expanded_path )
121
+ path_obj = pathlib . Path (path_str )
122
+ # Use Path methods instead of os.path
123
+ expanded_path = pathlib . Path ( os . path . expandvars ( str ( path_obj )))
124
+ return expanded_path . expanduser ( )
134
125
except Exception as e :
135
- raise ValueError (f"Invalid path: { v } . Error: { str (e )} " )
126
+ msg = f"Invalid path: { v } . Error: { e !s} "
127
+ raise ValueError (msg ) from e
136
128
137
129
@field_validator ("url" )
138
130
@classmethod
@@ -157,29 +149,30 @@ def validate_url(cls, v: str, info: t.Any) -> str:
157
149
If URL is invalid
158
150
"""
159
151
if not v :
160
- raise ValueError ("URL cannot be empty" )
152
+ msg = "URL cannot be empty"
153
+ raise ValueError (msg )
161
154
162
155
# Different validation based on VCS type
163
- values = info . data
164
- vcs_type = values .get ("vcs" , "" ).lower ()
156
+ # Keeping this but not using yet - can be expanded later
157
+ # vcs_type = values.get("vcs", "").lower()
165
158
166
159
# Basic validation for all URL types
167
160
if v .strip () == "" :
168
- raise ValueError ("URL cannot be empty or whitespace" )
161
+ msg = "URL cannot be empty or whitespace"
162
+ raise ValueError (msg )
169
163
170
164
# VCS-specific validation could be added here
171
165
# For now, just return the URL as is
172
166
return v
173
167
174
168
175
- class ConfigSectionModel ( BaseModel ):
169
+ class ConfigSectionDictModel ( RootModel [ dict [ str , RepositoryModel ]] ):
176
170
"""Configuration section model containing repositories.
177
171
178
- A section is a logical grouping of repositories, typically by project or organization.
172
+ A section is a logical grouping of repositories, typically by project or
173
+ organization.
179
174
"""
180
175
181
- __root__ : Dict [str , RepositoryModel ] = Field (default_factory = dict )
182
-
183
176
def __getitem__ (self , key : str ) -> RepositoryModel :
184
177
"""Get repository by name.
185
178
@@ -193,17 +186,17 @@ def __getitem__(self, key: str) -> RepositoryModel:
193
186
RepositoryModel
194
187
Repository configuration
195
188
"""
196
- return self .__root__ [key ]
189
+ return self .root [key ]
197
190
198
- def __iter__ (self ) -> t .Iterator [str ]:
199
- """Iterate over repository names.
191
+ def keys (self ) -> t .KeysView [str ]:
192
+ """Get repository names.
200
193
201
194
Returns
202
195
-------
203
- Iterator [str]
204
- Iterator of repository names
196
+ KeysView [str]
197
+ View of repository names
205
198
"""
206
- return iter ( self .__root__ )
199
+ return self .root . keys ( )
207
200
208
201
def items (self ) -> t .ItemsView [str , RepositoryModel ]:
209
202
"""Get items as name-repository pairs.
@@ -213,7 +206,7 @@ def items(self) -> t.ItemsView[str, RepositoryModel]:
213
206
ItemsView[str, RepositoryModel]
214
207
View of name-repository pairs
215
208
"""
216
- return self .__root__ .items ()
209
+ return self .root .items ()
217
210
218
211
def values (self ) -> t .ValuesView [RepositoryModel ]:
219
212
"""Get repository configurations.
@@ -223,18 +216,17 @@ def values(self) -> t.ValuesView[RepositoryModel]:
223
216
ValuesView[RepositoryModel]
224
217
View of repository configurations
225
218
"""
226
- return self .__root__ .values ()
219
+ return self .root .values ()
227
220
228
221
229
- class ConfigModel ( BaseModel ):
222
+ class ConfigDictModel ( RootModel [ dict [ str , ConfigSectionDictModel ]] ):
230
223
"""Complete configuration model containing sections.
231
224
232
- A configuration is a collection of sections, where each section contains repositories.
225
+ A configuration is a collection of sections, where each section contains
226
+ repositories.
233
227
"""
234
228
235
- __root__ : Dict [str , ConfigSectionModel ] = Field (default_factory = dict )
236
-
237
- def __getitem__ (self , key : str ) -> ConfigSectionModel :
229
+ def __getitem__ (self , key : str ) -> ConfigSectionDictModel :
238
230
"""Get section by name.
239
231
240
232
Parameters
@@ -244,40 +236,40 @@ def __getitem__(self, key: str) -> ConfigSectionModel:
244
236
245
237
Returns
246
238
-------
247
- ConfigSectionModel
239
+ ConfigSectionDictModel
248
240
Section configuration
249
241
"""
250
- return self .__root__ [key ]
242
+ return self .root [key ]
251
243
252
- def __iter__ (self ) -> t .Iterator [str ]:
253
- """Iterate over section names.
244
+ def keys (self ) -> t .KeysView [str ]:
245
+ """Get section names.
254
246
255
247
Returns
256
248
-------
257
- Iterator [str]
258
- Iterator of section names
249
+ KeysView [str]
250
+ View of section names
259
251
"""
260
- return iter ( self .__root__ )
252
+ return self .root . keys ( )
261
253
262
- def items (self ) -> t .ItemsView [str , ConfigSectionModel ]:
254
+ def items (self ) -> t .ItemsView [str , ConfigSectionDictModel ]:
263
255
"""Get items as section-repositories pairs.
264
256
265
257
Returns
266
258
-------
267
- ItemsView[str, ConfigSectionModel ]
259
+ ItemsView[str, ConfigSectionDictModel ]
268
260
View of section-repositories pairs
269
261
"""
270
- return self .__root__ .items ()
262
+ return self .root .items ()
271
263
272
- def values (self ) -> t .ValuesView [ConfigSectionModel ]:
264
+ def values (self ) -> t .ValuesView [ConfigSectionDictModel ]:
273
265
"""Get section configurations.
274
266
275
267
Returns
276
268
-------
277
- ValuesView[ConfigSectionModel ]
269
+ ValuesView[ConfigSectionDictModel ]
278
270
View of section configurations
279
271
"""
280
- return self .__root__ .values ()
272
+ return self .root .values ()
281
273
282
274
283
275
# Raw configuration models for initial parsing without validation
@@ -286,50 +278,89 @@ class RawRepositoryModel(BaseModel):
286
278
287
279
vcs : str
288
280
name : str
289
- path : Union [ str , Path ]
281
+ path : str | pathlib . Path
290
282
url : str
291
- remotes : Optional [ Dict [ str , Dict [str , Any ]]] = None
292
- shell_command_after : Optional [ List [ str ]] = None
283
+ remotes : dict [ str , dict [str , t . Any ]] | None = None
284
+ shell_command_after : list [ str ] | None = None
293
285
294
286
model_config = ConfigDict (
295
287
extra = "allow" , # Allow extra fields in raw config
296
288
str_strip_whitespace = True ,
297
289
)
298
290
299
291
300
- class RawConfigSectionModel ( BaseModel ):
301
- """Raw configuration section model before validation."""
292
+ # Use a type alias for the complex type in RawConfigSectionDictModel
293
+ RawRepoDataType = t . Union [ RawRepositoryModel , str , dict [ str , t . Any ]]
302
294
303
- __root__ : Dict [ str , Union [ RawRepositoryModel , str , Dict [ str , Any ]]] = Field (
304
- default_factory = dict
305
- )
295
+
296
+ class RawConfigSectionDictModel ( RootModel [ dict [ str , RawRepoDataType ]]):
297
+ """Raw configuration section model before validation."""
306
298
307
299
308
- class RawConfigModel ( BaseModel ):
300
+ class RawConfigDictModel ( RootModel [ dict [ str , RawConfigSectionDictModel ]] ):
309
301
"""Raw configuration model before validation and processing."""
310
302
311
- __root__ : Dict [str , RawConfigSectionModel ] = Field (default_factory = dict )
312
-
313
303
314
304
# Functions to convert between raw and validated models
315
305
def convert_raw_to_validated (
316
- raw_config : RawConfigModel ,
317
- cwd : t .Callable [[], Path ] = Path .cwd ,
318
- ) -> ConfigModel :
306
+ raw_config : RawConfigDictModel ,
307
+ cwd : t .Callable [[], pathlib . Path ] = pathlib . Path .cwd ,
308
+ ) -> ConfigDictModel :
319
309
"""Convert raw configuration to validated configuration.
320
310
321
311
Parameters
322
312
----------
323
- raw_config : RawConfigModel
313
+ raw_config : RawConfigDictModel
324
314
Raw configuration
325
315
cwd : Callable[[], Path], optional
326
316
Function to get current working directory, by default Path.cwd
327
317
328
318
Returns
329
319
-------
330
- ConfigModel
320
+ ConfigDictModel
331
321
Validated configuration
332
322
"""
333
- # Implementation will go here
334
- # This will handle shorthand syntax, variable resolution, etc.
335
- pass
323
+ # Create a new ConfigDictModel
324
+ config = ConfigDictModel (root = {})
325
+
326
+ # Process each section in the raw config
327
+ for section_name , raw_section in raw_config .root .items ():
328
+ # Create a new section in the validated config
329
+ config .root [section_name ] = ConfigSectionDictModel (root = {})
330
+
331
+ # Process each repository in the section
332
+ for repo_name , raw_repo_data in raw_section .root .items ():
333
+ # Handle string shortcuts (URL strings)
334
+ if isinstance (raw_repo_data , str ):
335
+ # Convert string URL to a repository model
336
+ repo_model = RepositoryModel (
337
+ vcs = "git" , # Default to git for string URLs
338
+ name = repo_name ,
339
+ path = cwd () / repo_name , # Default path is repo name in current dir
340
+ url = raw_repo_data ,
341
+ )
342
+ # Handle direct dictionary data
343
+ elif isinstance (raw_repo_data , dict ):
344
+ # Ensure name is set
345
+ if "name" not in raw_repo_data :
346
+ raw_repo_data ["name" ] = repo_name
347
+
348
+ # Validate and convert path
349
+ if "path" in raw_repo_data :
350
+ path = raw_repo_data ["path" ]
351
+ # Convert relative paths to absolute using cwd
352
+ path_obj = pathlib .Path (os .path .expandvars (str (path ))).expanduser ()
353
+ if not path_obj .is_absolute ():
354
+ path_obj = cwd () / path_obj
355
+ raw_repo_data ["path" ] = path_obj
356
+
357
+ # Create repository model
358
+ repo_model = RepositoryModel .model_validate (raw_repo_data )
359
+ else :
360
+ # Skip invalid repository data
361
+ continue
362
+
363
+ # Add repository to the section
364
+ config .root [section_name ].root [repo_name ] = repo_model
365
+
366
+ return config
0 commit comments