Skip to content

Commit d2eabd3

Browse files
committed
fix: resolve enum generation issues and test failures
1 parent 87dacb5 commit d2eabd3

5 files changed

Lines changed: 193 additions & 105 deletions

File tree

pyproject.toml

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
[build-system]
2-
requires = ["setuptools>=61.0", "wheel"]
3-
build-backend = "setuptools.build_meta"
2+
requires = ["poetry-core"]
3+
build-backend = "poetry.core.masonry.api"
44

55
[project]
66
name = "msgspec-schemaorg"
7-
version = "0.2.0"
7+
version = "0.2.1"
88
description = "Generate Python msgspec.Struct classes from the Schema.org vocabulary"
99
readme = "README.md"
1010
requires-python = ">=3.10"
@@ -36,6 +36,29 @@ dev = [
3636
"pytest"
3737
]
3838

39+
[tool.poetry]
40+
name = "msgspec-schemaorg"
41+
description = "Python msgspec.Struct models for Schema.org"
42+
authors = ["Mike Wolfson <mike@mikewolfson.com>"]
43+
readme = "README.md"
44+
homepage = "https://github.com/mikewolfd/msgspec-schemaorg"
45+
repository = "https://github.com/mikewolfd/msgspec-schemaorg"
46+
version = "0.2.1"
47+
packages = [{include = "msgspec_schemaorg"}]
48+
49+
[tool.poetry.dependencies]
50+
python = "^3.9"
51+
msgspec = ">=0.16.0"
52+
pydantic = ">=2.0.0"
53+
requests = ">=2.28.2"
54+
55+
[tool.poetry.group.dev.dependencies]
56+
black = "^24.1.1"
57+
isort = "^5.13.2"
58+
pytest = "^8.0.1"
59+
mypy = "^1.8.0"
60+
ruff = "^0"
61+
3962
[tool.setuptools]
4063
packages = ["msgspec_schemaorg", "msgspec_schemaorg.enums", "msgspec_schemaorg.enums.intangible"]
4164
include-package-data = true

scripts/generate_enums.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,19 +97,18 @@ def generate_enum_code(enum_type: str, values: list, category: str = None):
9797
if ':' in value_id:
9898
value_id = value_id.split(':')[-1]
9999

100-
# Clean comment for JSON embedding
100+
# Get comment and ensure it's a string
101101
comment = value.get('comment', '')
102102
if not isinstance(comment, str):
103103
comment = str(comment)
104104

105-
# Escape quotes in comment
106-
comment = comment.replace('"', '\\"')
107-
105+
# Use triple quotes for comment to properly handle multi-line text
108106
code.append(f' "{value_id}": {{')
109107
code.append(f' "id": "{value["id"]}",')
110-
code.append(f' "comment": "{comment}",')
108+
code.append(f' "comment": """{comment}""",')
111109
if value.get('label'):
112-
code.append(f' "label": "{value["label"]}",')
110+
label = str(value["label"]).replace('"', '\\"')
111+
code.append(f' "label": "{label}",')
113112
code.append(' },')
114113

115114
code.append(" }")

scripts/generate_models.py

Lines changed: 102 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,31 @@ def get_existing_enum_types():
6868
return enum_types
6969

7070

71+
def get_enum_categories():
72+
"""
73+
Get a mapping of enum types to their categories.
74+
75+
Returns:
76+
A dictionary mapping enum type names to their categories
77+
"""
78+
enum_categories = {}
79+
80+
if not ENUMS_DIR.exists():
81+
return enum_categories
82+
83+
for root, _, files in os.walk(ENUMS_DIR):
84+
category = os.path.basename(root)
85+
if category == "enums": # Skip the root enums directory
86+
continue
87+
88+
for file in files:
89+
if file.endswith('.py') and not file.startswith('__'):
90+
enum_name = os.path.splitext(file)[0]
91+
enum_categories[enum_name] = category
92+
93+
return enum_categories
94+
95+
7196
def modify_imports_for_enums(files, enum_types):
7297
"""
7398
Modify import statements in generated files to use enum types from the enums package.
@@ -76,17 +101,8 @@ def modify_imports_for_enums(files, enum_types):
76101
files: Dictionary mapping file paths to generated code
77102
enum_types: Set of type names that have enum implementations
78103
"""
79-
enum_locations = {}
80-
81-
# First, find all enum implementations and their locations
82-
for root, _, files_in_dir in os.walk(ENUMS_DIR):
83-
for file in files_in_dir:
84-
if file.endswith('.py') and not file.startswith('__'):
85-
enum_name = os.path.splitext(file)[0]
86-
# Get the category from the directory structure
87-
rel_path = os.path.relpath(root, ENUMS_DIR)
88-
category = rel_path if rel_path != '.' else ''
89-
enum_locations[enum_name] = category
104+
# Get enum categories
105+
enum_categories = get_enum_categories()
90106

91107
# Now update imports in all files
92108
for file_path, content in files.items():
@@ -97,20 +113,21 @@ def modify_imports_for_enums(files, enum_types):
97113

98114
# Look for import statements for enum types
99115
for enum_type in enum_types:
100-
if enum_type not in enum_locations:
116+
if enum_type not in enum_categories:
101117
continue
102118

103-
category = enum_locations[enum_type]
104-
category_path = f".{category}" if category else ""
119+
category = enum_categories[enum_type]
105120

106121
# Patterns for imports from models (handle various potential patterns)
107122
old_import_patterns = [
108123
f"from msgspec_schemaorg.models.intangible.{enum_type} import {enum_type}",
109-
f"from msgspec_schemaorg.models{category_path}.{enum_type} import {enum_type}"
124+
f"from msgspec_schemaorg.models.{category}.{enum_type} import {enum_type}",
125+
f"from .{enum_type} import {enum_type}",
126+
f"from ..{category}.{enum_type} import {enum_type}"
110127
]
111128

112129
# Pattern for corrected import from enums
113-
new_import = f"from msgspec_schemaorg.enums{category_path}.{enum_type} import {enum_type}"
130+
new_import = f"from msgspec_schemaorg.enums.{category}.{enum_type} import {enum_type}"
114131

115132
# Replace the import statement
116133
for old_pattern in old_import_patterns:
@@ -135,73 +152,28 @@ def modify_init_files(files, enum_types):
135152
continue
136153

137154
modified_content = content
155+
modified = False
138156

139157
# Remove imports for enum types
140158
for enum_type in enum_types:
141159
import_line = f"from .{enum_type} import {enum_type}\n"
142160
if import_line in modified_content:
143161
modified_content = modified_content.replace(import_line, "")
162+
modified = True
144163
print(f"Removed import for {enum_type} in {file_path}")
145164

146165
# Update __all__ list if present
147166
if "__all__ = [" in modified_content:
148167
# Find __all__ list
149168
all_list_start = modified_content.find("__all__ = [")
150169
all_list_end = modified_content.find("]", all_list_start)
151-
all_list = modified_content[all_list_start:all_list_end+1]
152-
153-
# Remove enum types from __all__ list
154-
for enum_type in enum_types:
155-
# Look for different patterns in __all__ list
156-
patterns = [
157-
f"'{enum_type}',",
158-
f"'{enum_type}'",
159-
f"\"{enum_type}\",",
160-
f"\"{enum_type}\""
161-
]
162-
163-
for pattern in patterns:
164-
if pattern in all_list:
165-
# Replace with empty string or just a space depending on location
166-
if pattern.endswith(","):
167-
all_list = all_list.replace(pattern, "")
168-
else:
169-
all_list = all_list.replace(pattern, "")
170-
print(f"Removed {enum_type} from __all__ list in {file_path}")
171-
172-
# Clean up any empty commas or double commas
173-
all_list = all_list.replace(",,", ",")
174-
all_list = all_list.replace(", ,", ",")
175-
all_list = all_list.replace("[,", "[")
176-
all_list = all_list.replace(",]", "]")
177-
178-
# Replace the old __all__ list with the cleaned up one
179-
modified_content = modified_content.replace(
180-
modified_content[all_list_start:all_list_end+1],
181-
all_list
182-
)
183-
184-
# Update the content in the files dictionary
185-
files[file_path] = modified_content
186-
187-
# Also update the root models/__init__.py if it exists
188-
root_init_path = Path(DEFAULT_OUTPUT_DIR) / "__init__.py"
189-
if root_init_path.exists():
190-
try:
191-
with open(root_init_path, 'r') as f:
192-
root_init_content = f.read()
193-
194-
modified_root_init = root_init_content
195-
196-
# Update __all__ list if present
197-
if "__all__ = [" in modified_root_init:
198-
# Find __all__ list
199-
all_list_start = modified_root_init.find("__all__ = [")
200-
all_list_end = modified_root_init.find("]", all_list_start)
201-
all_list = modified_root_init[all_list_start:all_list_end+1]
170+
if all_list_end > all_list_start: # Ensure we found the end bracket
171+
all_list = modified_content[all_list_start:all_list_end+1]
172+
original_all_list = all_list
202173

203174
# Remove enum types from __all__ list
204175
for enum_type in enum_types:
176+
# Look for different patterns in __all__ list
205177
patterns = [
206178
f"'{enum_type}',",
207179
f"'{enum_type}'",
@@ -213,7 +185,8 @@ def modify_init_files(files, enum_types):
213185
if pattern in all_list:
214186
# Replace with empty string
215187
all_list = all_list.replace(pattern, "")
216-
print(f"Removed {enum_type} from __all__ list in root __init__.py")
188+
modified = True
189+
print(f"Removed {enum_type} from __all__ list in {file_path}")
217190

218191
# Clean up any empty commas or double commas
219192
all_list = all_list.replace(",,", ",")
@@ -222,14 +195,68 @@ def modify_init_files(files, enum_types):
222195
all_list = all_list.replace(",]", "]")
223196

224197
# Replace the old __all__ list with the cleaned up one
225-
modified_root_init = modified_root_init.replace(
226-
modified_root_init[all_list_start:all_list_end+1],
227-
all_list
228-
)
198+
if all_list != original_all_list:
199+
modified_content = modified_content.replace(
200+
original_all_list,
201+
all_list
202+
)
203+
204+
# Update the content in the files dictionary if modified
205+
if modified:
206+
files[file_path] = modified_content
207+
208+
# Also update the root models/__init__.py if it exists
209+
root_init_path = Path(DEFAULT_OUTPUT_DIR) / "__init__.py"
210+
if root_init_path.exists():
211+
try:
212+
with open(root_init_path, 'r') as f:
213+
root_init_content = f.read()
214+
215+
modified_root_init = root_init_content
216+
modified = False
217+
218+
# Update __all__ list if present
219+
if "__all__ = [" in modified_root_init:
220+
# Find __all__ list
221+
all_list_start = modified_root_init.find("__all__ = [")
222+
all_list_end = modified_root_init.find("]", all_list_start)
223+
if all_list_end > all_list_start: # Ensure we found the end bracket
224+
all_list = modified_root_init[all_list_start:all_list_end+1]
225+
original_all_list = all_list
226+
227+
# Remove enum types from __all__ list
228+
for enum_type in enum_types:
229+
patterns = [
230+
f"'{enum_type}',",
231+
f"'{enum_type}'",
232+
f"\"{enum_type}\",",
233+
f"\"{enum_type}\""
234+
]
235+
236+
for pattern in patterns:
237+
if pattern in all_list:
238+
# Replace with empty string
239+
all_list = all_list.replace(pattern, "")
240+
modified = True
241+
print(f"Removed {enum_type} from __all__ list in root __init__.py")
242+
243+
# Clean up any empty commas or double commas
244+
all_list = all_list.replace(",,", ",")
245+
all_list = all_list.replace(", ,", ",")
246+
all_list = all_list.replace("[,", "[")
247+
all_list = all_list.replace(",]", "]")
248+
249+
# Replace the old __all__ list with the cleaned up one
250+
if all_list != original_all_list:
251+
modified_root_init = modified_root_init.replace(
252+
original_all_list,
253+
all_list
254+
)
229255

230-
# Write the modified content back to the file
231-
with open(root_init_path, 'w') as f:
232-
f.write(modified_root_init)
256+
# Write the modified content back to the file if changed
257+
if modified:
258+
with open(root_init_path, 'w') as f:
259+
f.write(modified_root_init)
233260
except Exception as e:
234261
print(f"Error updating root __init__.py: {e}")
235262

tests/test_schema_examples.py

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
"""
55

66
import importlib
7+
import importlib.util
8+
import pkgutil # Import pkgutil for iterating modules
79
import inspect
810
import json
911
import os
1012
import re
1113
import sys
1214
from collections.abc import Iterator
13-
from typing import Union, Any, Dict, List, Optional
15+
from typing import Union, Any, Dict, List, Optional, Set, Type, get_type_hints
1416

1517
import requests
1618
from requests import Response
@@ -54,22 +56,31 @@ def get_modules_in_package(package_name: str) -> Iterator[Any]:
5456
# Get all modules in the package
5557
if hasattr(package, "__path__"):
5658
package_path = package.__path__
57-
for _, module_name, is_pkg in importlib.iter_modules(package_path):
58-
full_module_name = f"{package_name}.{module_name}"
59-
try:
60-
module = importlib.import_module(full_module_name)
61-
62-
# If this is a package, get all classes from it
63-
if is_pkg:
64-
yield from get_modules_in_package(full_module_name)
65-
66-
# Get all classes from this module
67-
for name in dir(module):
68-
attr = getattr(module, name)
69-
if isinstance(attr, type) and issubclass(attr, msgspec.Struct) and attr.__module__ == module.__name__:
70-
yield attr
71-
except ImportError:
72-
continue
59+
for module_info in pkgutil.iter_modules(package_path): # Use pkgutil.iter_modules
60+
module_name = module_info.name # Access name attribute
61+
is_pkg = module_info.ispkg # Access ispkg attribute
62+
63+
if is_pkg:
64+
# If module is a package, recurse into it
65+
sub_package = f"{package_name}.{module_name}"
66+
try:
67+
yield from get_modules_in_package(sub_package)
68+
except (ImportError, ModuleNotFoundError):
69+
continue
70+
else:
71+
try:
72+
# Try to import the module
73+
module = importlib.import_module(f"{package_name}.{module_name}")
74+
75+
# Find all class objects defined in the module
76+
for name, obj in inspect.getmembers(module):
77+
if inspect.isclass(obj) and obj.__module__ == module.__name__:
78+
# Skip internal classes
79+
if not name.startswith("_"):
80+
yield obj
81+
except (ImportError, ModuleNotFoundError):
82+
# Skip if module can't be imported
83+
continue
7384
else:
7485
# This is a module, not a package
7586
for name in dir(package):

0 commit comments

Comments
 (0)