Markdown Converter
Agent skill for markdown-converter
This document captures critical knowledge, patterns, and best practices for working with the hwpapi codebase. Follow these guidelines to work effectively and avoid common pitfalls.
Sign in to like and favorite skills
This document captures critical knowledge, patterns, and best practices for working with the hwpapi codebase. Follow these guidelines to work effectively and avoid common pitfalls.
ALL Python files in
hwpapi/ are AUTO-GENERATED from Jupyter notebooks. Editing them directly will result in lost changes.
β WRONG: Edit hwpapi/parametersets.py β RIGHT: Edit nbs/02_api/02_parameters.ipynb, then run nbdev_export
| Notebook | Generated Python File |
|---|---|
| |
| |
| |
| |
| |
| |
# 1. Identify the .py file that needs changes # 2. Find corresponding .ipynb file (see mapping above) # 3. Edit the notebook # 4. Export to regenerate .py files nbdev_export # 5. Test your changes python -m pytest tests/ # 6. Commit BOTH notebook and generated .py file git add nbs/02_api/02_parameters.ipynb hwpapi/parametersets.py git commit -m "Your change description"
# AUTOGENERATED! DO NOT EDIT! at the topnbdev_exportThe codebase uses multiple backends to handle different parameter set types:
# Backend hierarchy ParameterBackend (Protocol) βββ PsetBackend # For pset objects (preferred, modern) βββ HParamBackend # For HParameterSet (legacy) βββ ComBackend # For generic COM objects βββ AttrBackend # For plain Python objects
Key Functions:
_is_com(obj) - Checks if object is COM (has _oleobj_ or 'com_gen_py')_looks_like_pset(obj) - Checks for pset-specific methods (Item, SetItem, CreateItemSet)make_backend(obj) - Factory that auto-detects and returns appropriate backendImportant: Backend selection is automatic. Trust the factory function.
Type-safe properties with automatic validation and conversion:
class CharShape(ParameterSet): bold = BoolProperty("Bold", "Bold formatting") fontsize = IntProperty("Size", "Font size in points") text_color = ColorProperty("TextColor", "Text color")
Property Types:
IntProperty - Integer valuesBoolProperty - Boolean valuesStringProperty - String valuesColorProperty - Hex color β HWP color conversionUnitProperty - π Smart unit conversion (mm, cm, in, pt β HWPUNIT)MappedProperty - String β Integer via mapping dictTypedProperty - Nested ParameterSet (manual)ListProperty - List of values (basic Python lists)NestedProperty - π Auto-creating nested ParameterSet with tab completionArrayProperty - π Auto-creating HArray with list-like interfaceAuto-Generated Attributes:
attributes_names property returns list(self._property_registry.keys())_property_registryself.attributes_names = [...] in subclassesAuto-Creating Properties (New Pattern):
NestedProperty and ArrayProperty automatically create underlying COM objectscreate_itemset() or array initialization neededTwo execution modes:
Pset-based (Modern, Preferred)
HSet-based (Legacy)
apply() to commitCode Pattern:
# Check backend type if isinstance(self._backend, PsetBackend): # Immediate mode else: # Staging mode - accumulate in self._staged
Symptom:
NameError: name '_is_com' is not defined
Cause: Helper function used but not defined in notebook
Solution:
# Add the missing function in the notebook with #| export #| export def _is_com(obj: Any) -> bool: """Check if object is a COM object.""" if obj is None: return False return hasattr(obj, '_oleobj_') or 'com_gen_py' in str(type(obj))
Symptom:
AttributeError: property 'attributes_names' of 'X' object has no setter
Cause: Trying to set
self.attributes_names = [...] after it became a read-only property
Solution: Define properties instead of setting attributes_names:
# β OLD WAY (broken) class MyPS(ParameterSet): def __init__(self): super().__init__() self.attributes_names = ["a", "b"] self.a = None self.b = None # β NEW WAY (correct) class MyPS(ParameterSet): a = IntProperty("a", "Value a") b = IntProperty("b", "Value b") def __init__(self): super().__init__() # attributes_names auto-generated from properties
Symptom:
AttributeError: 'NoneType' object has no attribute 'delete'
Cause: Unbound ParameterSet (created without COM object)
Solution: Add None checks:
def _del_value(self, name): """Legacy method - use backend instead.""" if self._backend is None: return False return self._backend.delete(name)
Symptom: Circular import or missing imports
Solution: Check notebook cell order and exports:
#| export directive to export functionsfrom typing import TYPE_CHECKING for type-only importsSymptom: Method doesn't behave as expected after edits; old logic still runs despite changes
Cause: Entire class or methods duplicated in notebook cell (common during copy-paste refactoring)
How to Detect:
# Count occurrences of method definition in generated file python -c "with open('hwpapi/parametersets.py', encoding='utf-8') as f: print(f.read().count('def _format_int_value'))" # Output: 2 (should be 1!) # Find which cell has duplicates in notebook python -c "import json; nb=json.load(open('nbs/02_api/02_parameters.ipynb', encoding='utf-8')); cell=nb['cells'][26]; print(f'Method appears {cell[\"source\"].count(\"def _format_int_value\")} times in cell 26')"
Solution:
# Identify duplicate boundaries python << 'EOF' import json nb = json.load(open('nbs/02_api/02_parameters.ipynb', encoding='utf-8')) cell = nb['cells'][26] source = ''.join(cell['source']) # Find all method definitions import re methods = [(m.start(), m.group(1)) for m in re.finditer(r'\n def (\w+)', source)] # Look for duplicate method names from collections import Counter method_counts = Counter(name for _, name in methods) duplicates = {name: count for name, count in method_counts.items() if count > 1} if duplicates: print(f"DUPLICATES FOUND: {duplicates}") print("\nRemove duplicate definitions manually from the notebook cell.") else: print("No duplicates found.") EOF # After removing duplicates, export and verify nbdev_export python -c "with open('hwpapi/parametersets.py', encoding='utf-8') as f: print(f'Now has {f.read().count(\"def _format_int_value\")} definition(s)')"
Real Example:
ParameterSet class was duplicated in cell 26 (27,304 characters)_format_int_value had old logic: 'Size' in prop_name and 'Font' not in prop_name_format_int_value had correct logic: 'Size' in prop_name or prop_name.endswith('Size')FontSize to display as 1200 instead of 12.0ptPrevention:
.py file after major refactoringgrep -c "def method_name" hwpapi/parametersets.py to count definitionsBefore:
class CharShape(ParameterSet): def __init__(self): super().__init__() self.attributes_names = [ "facename_hangul", "facename_latin", ..., # 67 lines! ]
After:
# In ParameterSet base class @property def attributes_names(self): """Auto-generated list of attribute names from property registry.""" return list(self._property_registry.keys()) # In subclasses - just define properties, no manual list! class CharShape(ParameterSet): facename_hangul = StringProperty("FaceNameHangul", "...") facename_latin = StringProperty("FaceNameLatin", "...") # No self.attributes_names needed!
Result: Removed ~500 lines, eliminated maintenance burden
PropertyDescriptor("key", doc, converter=int)TYPE_CHECKING or reorder definitionstests/test_hparam.py βββ Unit tests (with mocks) - Run without HWP βββ Integration tests - Require HWP installed βββ Graceful skipping - Tests skip if dependencies unavailable
# All tests python -m pytest tests/test_hparam.py -v # Specific test class python -m pytest tests/test_hparam.py::TestParameterSetUpdateFrom -v # Show skipped tests python -m pytest tests/test_hparam.py -v -ra
For Unit Tests: Just Python + pytest For Integration Tests:
import unittest from hwpapi.parametersets import ParameterSet, IntProperty class TestMyFeature(unittest.TestCase): def test_feature(self): # Use real ParameterSet subclasses that have properties defined from hwpapi.parametersets import CharShape ps = CharShape() ps.bold = True self.assertEqual(ps.bold, True)
Important: When testing ParameterSet, use classes with actual property descriptors, not manual attributes_names lists.
#| export class MyParameterSet(ParameterSet): """ ### MyParameterSet 123) MyParameterSet : λ΄ νλΌλ―Έν°μ Description of what this parameter set does. """ # Define properties (NOT attributes_names) my_int = IntProperty("MyInt", "Integer value") my_bool = BoolProperty("MyBool", "Boolean flag") my_color = ColorProperty("MyColor", "Color value") def __init__(self, parameterset=None, **kwargs): super().__init__(parameterset, **kwargs) # NO self.attributes_names = [...] needed! # Stage initial values if needed if 'my_int' in kwargs: self.my_int = kwargs['my_int']
#| export class NestedProperty(PropertyDescriptor): """ Auto-creating nested ParameterSet property. Automatically calls CreateItemSet when first accessed. Example: class FindReplace(ParameterSet): find_char_shape = NestedProperty("FindCharShape", "CharShape", CharShape) pset = FindReplace(action.CreateSet()) pset.find_char_shape.bold = True # Auto-creates! Tab completion works! """ def __init__(self, key: str, setid: str, param_class: Type["ParameterSet"], doc: str = ""): super().__init__(key, doc) self.setid = setid self.param_class = param_class self._cache_attr = f"_nested_cache_{key}" def __get__(self, instance, owner): if instance is None: return self # Check cache first if hasattr(instance, self._cache_attr): return getattr(instance, self._cache_attr) # Auto-create via CreateItemSet if instance._backend and hasattr(instance._backend, 'create_itemset'): nested_pset_com = instance._backend.create_itemset(self.key, self.setid) nested_wrapped = self.param_class(nested_pset_com) else: # Fallback: create unbound instance nested_wrapped = self.param_class() # Cache for future access setattr(instance, self._cache_attr, nested_wrapped) return nested_wrapped
Usage:
class FindReplace(ParameterSet): find_string = StringProperty("FindString", "Text to find") find_char_shape = NestedProperty("FindCharShape", "CharShape", CharShape) pset = FindReplace(action.CreateSet()) pset.find_char_shape.bold = True # Simple! Tab completion works!
#| export class MyProperty(PropertyDescriptor): """Custom property with special conversion.""" def __get__(self, instance, owner): if instance is None: return self value = self._get_value(instance) if value is None: return self.default # Your conversion logic return my_conversion(value) def __set__(self, instance, value): if value is None: self._del_value(instance) return # Your validation logic converted = my_conversion(value) self._set_value(instance, converted)
# Always use the helper function if _is_com(obj): # Handle COM object # For pset specifically if _looks_like_pset(obj): # Handle pset object # Let factory decide backend = make_backend(obj) # Automatic detection
def my_method(self): """Method that accesses backend.""" if self._backend is None: # Handle unbound case return default_value # Proceed with backend operations return self._backend.get(self.key)
Problem: Manual nested parameter set creation is verbose and breaks tab completion:
# β Old way - too complicated! pset = FindReplace(action.CreateSet()) char_com = pset.create_itemset("FindCharShape", "CharShape") char_shape = CharShape(char_com) char_shape.bold = True
Solution: Auto-creating properties that work like regular Python attributes:
# β New way - simple and intuitive! pset = FindReplace(action.CreateSet()) pset.find_char_shape.bold = True # Auto-creates! Tab completion works!
Purpose: Automatically create nested parameter sets via
CreateItemSet on first access.
Signature:
NestedProperty(key: str, setid: str, param_class: Type[ParameterSet], doc: str = "")
Parameters:
key - Parameter key in HWP (e.g., "FindCharShape")setid - SetID for CreateItemSet call (e.g., "CharShape")param_class - ParameterSet class to wrap (e.g., CharShape)doc - Documentation stringExample Definition:
class FindReplace(ParameterSet): """Find and replace parameters.""" # Simple properties find_string = StringProperty("FindString", "Text to find") # Auto-creating nested properties find_char_shape = NestedProperty("FindCharShape", "CharShape", CharShape, "Character formatting to find") find_para_shape = NestedProperty("FindParaShape", "ParaShape", ParaShape, "Paragraph formatting to find")
Example Usage:
pset = app.actions.repeat_find.create_set() # Access nested property - auto-creates CharShape via CreateItemSet! pset.find_char_shape.bold = True pset.find_char_shape.italic = False pset.find_char_shape.text_color = "#FF0000" # IDE provides full tab completion on find_char_shape! # No manual create_itemset() call needed!
How It Works:
pset.find_char_shape triggers NestedProperty.__get__backend.create_itemset("FindCharShape", "CharShape") to create COM objectCharShape Python classBenefits:
Purpose: Automatically convert between user-friendly units (mm, cm, in, pt) and HWPUNIT.
Problem: HWPUNIT is not intuitive (1mm = 283 HWPUNIT, 1pt = 100 HWPUNIT)
Solution: Accept familiar units, auto-convert internally
Signature:
UnitProperty(key: str, doc: str, default_unit: str = "mm", output_unit: Optional[str] = None, min_value: Optional[float] = None, max_value: Optional[float] = None)
Example Definition:
class PageDef(ParameterSet): """Page layout.""" # Dimensions in millimeters (most common for paper) width = UnitProperty("Width", "Page width", default_unit="mm") height = UnitProperty("Height", "Page height", default_unit="mm") # Margins in millimeters left_margin = UnitProperty("LeftMargin", "Left margin", default_unit="mm") class CharShape(ParameterSet): """Character formatting.""" # Font size in points (standard for typography) fontsize = UnitProperty("Height", "Font size", default_unit="pt")
Example Usage:
# Page dimensions - ALL of these work! page = PageDef(action.CreateSet()) # String with unit (most explicit) page.width = "210mm" # A4 width page.height = "297mm" # A4 height # Different units (auto-converts) page.width = "21cm" # Same as 210mm page.width = "8.27in" # Same as 210mm # Bare number (uses default_unit = mm) page.width = 210 # Assumes mm # Set margins with mixed units page.left_margin = 25 # 25mm (bare number) page.right_margin = "2.5cm" # 25mm (converted) page.top_margin = "1in" # ~25.4mm (converted) # Get value (returns in mm) print(f"Width: {page.width}mm") # Output: Width: 210.0mm # Font size in points char = CharShape(action.CreateSet()) char.fontsize = 12 # 12pt char.fontsize = "12pt" # Same char.fontsize = "4.23mm" # Converts to pt internally print(f"Font: {char.fontsize}pt") # Output: Font: 12.0pt
Supported Units:
mm - Millimeters (1mm = 283 HWPUNIT) - Default for dimensionscm - Centimeters (1cm = 2830 HWPUNIT)in - Inches (1in = 7200 HWPUNIT)pt - Points (1pt = 100 HWPUNIT) - Default for fontsBenefits:
Purpose: Provide Pythonic list interface for HWP's HArray (PIT_ARRAY) parameters.
Signature:
ArrayProperty(key: str, item_type: Type, doc: str = "", min_length: Optional[int] = None, max_length: Optional[int] = None)
Parameters:
key - Parameter key in HWP (e.g., "TabStops", "Point")item_type - Type of array elements (int, float, str, tuple, etc.)doc - Documentation stringmin_length - Minimum array length (optional validation)max_length - Maximum array length (optional validation)Example Definition:
class TabDef(ParameterSet): """Tab definition.""" # Array of tab stop positions (in HWPUNIT) tab_stops = ArrayProperty("TabStops", int, "Tab stop positions") class BorderFill(ParameterSet): """Border and fill settings.""" # Array of 4 border widths: [left, right, top, bottom] border_widths = ArrayProperty("BorderWidth", int, "Border widths for each side", min_length=4, max_length=4) class DrawCoordInfo(ParameterSet): """Drawing coordinates.""" # Array of (X, Y) coordinate tuples points = ArrayProperty("Point", tuple, "Coordinate points")
Example Usage:
# Tab stops tab_def = TabDef(action.CreateSet()) tab_def.tab_stops = [1000, 2000, 3000, 4000] # Set entire array tab_def.tab_stops.append(5000) # Standard list method print(tab_def.tab_stops[0]) # Index access: 1000 # Border widths border = BorderFill(action.CreateSet()) border.border_widths = [10, 10, 20, 20] # left, right, top, bottom border.border_widths[2] = 25 # Update top border # Coordinates coords = DrawCoordInfo(action.CreateSet()) coords.points = [(0, 0), (100, 100), (200, 50)] coords.points.append((300, 75)) for i, (x, y) in enumerate(coords.points): print(f"Point {i}: ({x}, {y})")
List-Like Methods:
# HArrayWrapper provides full list interface: array.append(item) # Add to end array.insert(index, item) # Insert at position array.remove(item) # Remove first occurrence array.pop(index) # Remove and return array.clear() # Remove all array[index] # Get item array[index] = value # Set item len(array) # Array length for item in array: ... # Iteration
How It Works:
array = [...] triggers ArrayProperty.__set__HArrayWrapper instance wrapping COM HArrayBenefits:
item_typeclass AdvancedTable(ParameterSet): """Table with all property types demonstrated.""" # Simple properties rows = IntProperty("Rows", "Number of rows") cols = IntProperty("Cols", "Number of columns") has_header = BoolProperty("HasHeader", "First row is header") title = StringProperty("Title", "Table title") align = MappedProperty("Align", "Alignment", ALIGN_MAP) # Unit properties - AUTO-CONVERTING! table_width = UnitProperty("Width", "Table width", default_unit="mm") table_height = UnitProperty("Height", "Table height", default_unit="mm") # Array properties - AUTO-CREATING! column_widths = ArrayProperty("ColWidths", int, "Width of each column in HWPUNIT") row_heights = ArrayProperty("RowHeights", int, "Height of each row in HWPUNIT") # Nested property - AUTO-CREATING! border_fill = NestedProperty("BorderFill", "BorderFill", BorderFill, "Border and fill settings") # Usage - everything just works! table = AdvancedTable(action.CreateSet()) # Simple properties table.rows = 3 table.cols = 4 table.has_header = True table.title = "Sales Report" table.align = "center" # Unit properties (auto-converts to HWPUNIT) table.table_width = "150mm" # String with unit table.table_height = 80 # Bare number, assumes mm # OR use different units: table.table_width = "15cm" # Same as 150mm table.table_width = "5.91in" # Same as 150mm # Array assignments (auto-creates HArray) table.column_widths = [2000, 3000, 2500, 2000] # HWPUNIT values table.row_heights = [1000, 1000, 1000] # Array modifications table.column_widths.append(1500) table.column_widths[2] = 3500 # Nested object access (auto-creates BorderFill via CreateItemSet) table.border_fill.border_type = "solid" table.border_fill.fill_color = "#EEEEEE" # If border_fill has UnitProperty for border widths: table.border_fill.border_left = "2mm" table.border_fill.border_right = "0.2cm" # Same as 2mm # Execute table.run()
Before:
class FindReplace(ParameterSet): find_char_shape = TypedProperty("FindCharShape", "Character formatting", CharShape) # Usage - manual creation pset = FindReplace(action.CreateSet()) char_com = pset.create_itemset("FindCharShape", "CharShape") char_shape = CharShape(char_com) char_shape.bold = True
After:
class FindReplace(ParameterSet): find_char_shape = NestedProperty("FindCharShape", "CharShape", CharShape, "Character formatting") # Usage - automatic! pset = FindReplace(action.CreateSet()) pset.find_char_shape.bold = True # Auto-creates!
Migration Steps:
TypedProperty(key, doc, ParamClass) to NestedProperty(key, setid, ParamClass, doc)setid parameter (usually matches the class name)create_itemset() calls in usage codeBefore:
class TabDef(ParameterSet): tab_stops = ListProperty("TabStops", "Tab positions", item_type=int) # Usage - basic Python list (no COM sync) tab_def = TabDef() tab_def.tab_stops = [1000, 2000, 3000]
After:
class TabDef(ParameterSet): tab_stops = ArrayProperty("TabStops", int, "Tab positions") # Usage - syncs with HArray tab_def = TabDef(action.CreateSet()) tab_def.tab_stops = [1000, 2000, 3000] # Syncs to COM HArray
Key Differences:
ArrayProperty requires binding to COM object (HArray)ListProperty is pure Python (no COM sync)ArrayProperty for HWP parameters that are PIT_ARRAY typeListProperty for internal Python-only listsDoes this parameter exist in HWP documentation? ββ NO β Use regular Python attribute or ListProperty ββ YES β What type is it? ββ Simple value (int, bool, string) β Use IntProperty, BoolProperty, StringProperty ββ Enum/mapped value β Use MappedProperty ββ Nested ParameterSet β Use NestedProperty (auto-creating!) ββ Array (PIT_ARRAY) β Use ArrayProperty (auto-creating!) ββ Unit value (HWPUNIT) β Use UnitProperty (auto-converts mm/cm/in/pt!) ββ Color value β Use ColorProperty
Unit Selection Guide:
default_unit="mm"default_unit="mm"default_unit="pt"default_unit="mm"default_unit="pt" or "mm"When adding auto-creating properties to a ParameterSet class:
For NestedProperty:
SetID for CreateItemSet (usually matches class name)name = NestedProperty(key, setid, ParamClass, doc)pset.name.some_property = value works without manual creationFor ArrayProperty:
name = ArrayProperty(key, item_type, doc, min_length, max_length)pset.name = [...] and pset.name.append(...) workDO β :
NestedProperty for all nested ParameterSets (not TypedProperty)ArrayProperty for HWP array parameters (not ListProperty)setid matching HWP documentationDON'T β:
TypedProperty for new code (use NestedProperty)create_itemset() when using NestedPropertyListProperty for HWP array parameters (use ArrayProperty)key and setid parametersThe
ParameterSet.__repr__() method has been enhanced with three powerful features that create self-documenting, human-readable output. These enhancements work together to make debugging and learning much easier.
Purpose: Convert internal HWP values to intuitive, human-readable formats.
Conversions:
| Property Type | Internal Value | Display Format | Conversion |
|---|---|---|---|
| Colors | (BBGGRR) | | BBGGRR β #RRGGBB hex |
| Font Sizes | (HWPUNIT) | | HWPUNIT Γ· 100 |
| Dimensions | (HWPUNIT) | | via |
| Booleans | / | / | Direct display |
Implementation:
_format_int_value() method detects property typeColorProperty, UnitProperty, etc.Example:
pset = CharFormat() pset.FontSize = 1200 pset.TextColor = 0x0000FF pset.Width = 59430 print(pset) # Output: # CharFormat( # FontSize=12.0pt # TextColor="#ff0000" # Width=210.0mm # )
Purpose: Show both numeric value and mapped name for enum-like properties.
Format:
{numeric_value} ({mapped_name})
How It Works:
MappedPropertyvalue (name)Example:
class BookMark(ParameterSet): Type = MappedProperty("Type", { "μΌλ°μ± κ°νΌ": 0, "λΈλ‘μ± κ°νΌ": 1 }, "Bookmark type") bookmark = BookMark() bookmark.Type = "λΈλ‘μ± κ°νΌ" # Set using string name print(bookmark) # Output: # BookMark( # Type=1 (λΈλ‘μ± κ°νΌ) # ... # )
Benefits:
Common Use Cases:
# Search direction Direction=0 (down) Direction=1 (up) Direction=2 (all) # Text alignment Align=0 (left) Align=1 (center) Align=2 (right) Align=3 (justify) # Bookmark types (Korean) Type=0 (μΌλ°μ± κ°νΌ) Type=1 (λΈλ‘μ± κ°νΌ)
Purpose: Display inline documentation for every property.
Format:
property=value # description
How It Works:
doc attributeExample:
class VideoInsert(ParameterSet): Base = StringProperty("Base", "λμμ νμΌμ κ²½λ‘") Format = MappedProperty("Format", {"mp4": 0, "avi": 1}, "λμμ νμ") Width = IntProperty("Width", "λμμ λλΉ (HWPUNIT)") video = VideoInsert() video.Base = "C:/Videos/sample.mp4" video.Format = "mp4" video.Width = 59430 print(video) # Output: # VideoInsert( # Base="C:/Videos/sample.mp4" # λμμ νμΌμ κ²½λ‘ # Format=0 (mp4) # λμμ νμ # Width=210.0mm # λμμ λλΉ (HWPUNIT) # )
Benefits:
Complete Example:
class CharFormat(ParameterSet): FontName = StringProperty("FontName", "Font family name") FontSize = IntProperty("FontSize", "Font size in HWPUNIT (100 = 1pt)") TextColor = ColorProperty("TextColor", "Text color in BBGGRR format") Bold = BoolProperty("Bold", "Bold formatting") Underline = MappedProperty("Underline", { "none": 0, "single": 1, "double": 2 }, "Underline style") char = CharFormat() char.FontName = "Arial" char.FontSize = 1200 char.TextColor = 0x0000FF char.Bold = True char.Underline = "single" print(char) # Output: # CharFormat( # Bold=True # Bold formatting # FontName="Arial" # Font family name # FontSize=12.0pt # Font size in HWPUNIT (100 = 1pt) # TextColor="#ff0000" # Text color in BBGGRR format # Underline=1 (single) # Underline style # [staged changes: 5] # )
Notice:
12.0pt - Human-readable value (Enhancement 1)#ff0000 - Color converted to hex (Enhancement 1)1 (single) - Enum shows value + name (Enhancement 2)# Font size... - Description explains everything (Enhancement 3)Location:
ParameterSet._format_repr() method in nbs/02_api/02_parameters.ipynb (Cell 26)
Key Methods:
def __repr__(self): """Return human-readable representation.""" return self._format_repr() def _format_repr(self, indent=0, max_depth=3): """Format ParameterSet with all enhancements.""" # 1. Get all properties from registry # 2. Format each value based on type # 3. Add enum display for MappedProperty # 4. Append description comment # 5. Return complete formatted string def _format_int_value(self, prop_name, prop_descriptor, value): """Format integer values based on property type.""" # Detect colors, sizes, dimensions # Apply appropriate conversion # Return formatted string
Testing:
# Run demos python examples/nested_property_demo.py python examples/mapped_property_display_demo.py python examples/property_description_display_demo.py
DO β :
# Provide clear, informative descriptions FontSize = IntProperty("FontSize", "Font size in HWPUNIT (100 = 1pt)") Width = IntProperty("Width", "Table width in HWPUNIT (283 = 1mm)") Rows = IntProperty("Rows", "Number of rows (1-500)") # Include units, formats, ranges in descriptions TextColor = ColorProperty("TextColor", "Text color in BBGGRR format") Direction = MappedProperty("Direction", {...}, "Search direction (down=forward, up=backward)") # Use descriptive enum values Align = MappedProperty("Align", { "left": 0, "center": 1, "right": 2 }, "Text alignment on page")
DON'T β:
# Don't leave descriptions empty FontSize = IntProperty("FontSize", "") # No help for users! # Don't omit units/constraints Width = IntProperty("Width", "Width") # Width in what unit? # Don't use cryptic enum values Mode = MappedProperty("Mode", { "m1": 0, # What is m1? "m2": 1 # What is m2? }, "Mode")
For Users:
For Developers:
For Documentation:
nbdev_export after notebook changes#| export directive to cells that should be exportedfrom __future__ import annotations)self.attributes_names = [...] in subclassesisinstance checks unless necessary#| export_is_com)# 1. Identify what needs changing # - Read the .py file to understand current code # - Find the corresponding .ipynb file # 2. Edit the notebook # - Open nbs/02_api/XX_name.ipynb # - Make your changes in the appropriate cell # - Add #| export if creating new exported code # 3. Export to Python nbdev_export # 4. Verify the generated code # - Check that hwpapi/*.py has your changes # - Look for any warnings or errors # 5. Test your changes python -c "import hwpapi; from hwpapi.parametersets import CharShape" python -m pytest tests/test_hparam.py -v # 6. Test in actual use (if possible) python examples/your_example.py # 7. Commit both files git add nbs/02_api/XX_name.ipynb hwpapi/name.py git commit -m "Description of change"
Save this as
verify_changes.sh:
#!/bin/bash echo "=== Running nbdev_export ===" nbdev_export echo -e "\n=== Testing imports ===" python -c "import hwpapi; from hwpapi.parametersets import CharShape, BorderFill; print('β Imports successful')" echo -e "\n=== Running tests ===" python -m pytest tests/test_hparam.py -v --tb=short echo -e "\n=== Checking for common issues ===" grep -r "self.attributes_names = \[" nbs/ && echo "β Found manual attributes_names!" || echo "β No manual attributes_names" echo -e "\nβ Verification complete!"
| File | Purpose |
|---|---|
| nbdev configuration, version, metadata |
| Claude Code spec file (project overview) |
| This file - working guidelines |
| Context on pset refactoring |
| Recent refactoring documentation |
| Duplicate class bug fix and display formatting (2025-12-09) |
| NestedProperty & ArrayProperty design |
| Smart unit conversion specification |
| Class | Location | Purpose |
|---|---|---|
| | High-level API for users |
| | Mid-level wrapper around HwpObject |
| | Base class for all parameter sets |
| | Base for property types |
| | Metaclass for auto-registration |
| Function | Location | Purpose |
|---|---|---|
| | Check if object is COM |
| | Check if object is pset |
| | Create appropriate backend |
| | Resolve action arguments |
# Logging Configuration HWPAPI_LOG_LEVEL=DEBUG # Set logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) # Default: WARNING (production-friendly, only shows warnings/errors) # Use DEBUG or INFO for development/troubleshooting # Examples: # Development - show all logs export HWPAPI_LOG_LEVEL=DEBUG # Production - only warnings and errors (default if not set) export HWPAPI_LOG_LEVEL=WARNING # Quiet mode - only errors and critical export HWPAPI_LOG_LEVEL=ERROR
Important: The default log level is
WARNING, which means normal users only see warnings, errors, and critical messages. This is intentional to avoid cluttering output in production. Set HWPAPI_LOG_LEVEL=DEBUG or INFO when you need detailed logging for development or troubleshooting.
# Force re-export rm hwpapi/*.py nbdev_export # Check for syntax errors in notebook python -m nbformat.validate nbs/02_api/02_parameters.ipynb
# Check what's exported python -c "import hwpapi.parametersets; print(dir(hwpapi.parametersets))" # Check __all__ in generated file grep "__all__" hwpapi/parametersets.py
# Run with verbose output python -m pytest tests/test_hparam.py -vv -s # Run specific test python -m pytest tests/test_hparam.py::TestClass::test_method -vv # Show full traceback python -m pytest tests/test_hparam.py --tb=long
# Validate notebook JSON python -m nbformat.validate nbs/02_api/02_parameters.ipynb # If corrupted, restore from git git checkout nbs/02_api/02_parameters.ipynb
# Quick check for duplicate method names in generated file grep -c "def _format_int_value" hwpapi/parametersets.py # Should be 1 # Find all duplicate methods in a file python << 'EOF' import re from collections import Counter with open('hwpapi/parametersets.py', encoding='utf-8') as f: content = f.read() # Find all method definitions methods = re.findall(r'\n def (\w+)', content) method_counts = Counter(methods) # Show duplicates duplicates = {name: count for name, count in method_counts.items() if count > 1} if duplicates: print("DUPLICATES FOUND:") for name, count in duplicates.items(): print(f" {name}: {count} times") else: print("No duplicates found.") EOF # Find which notebook cell has the duplicate python -c " import json nb = json.load(open('nbs/02_api/02_parameters.ipynb', encoding='utf-8')) for i, cell in enumerate(nb['cells']): source = ''.join(cell.get('source', [])) count = source.count('def _format_int_value') if count > 0: print(f'Cell {i}: {count} definition(s)') " # After fixing, verify nbdev_export grep -c "def _format_int_value" hwpapi/parametersets.py # Should be 1 now
Auto-generation beats manual maintenance
attributes_names property eliminated 500+ linesProperty system is powerful
Edge cases matter
nbdev workflow is non-negotiable
Simplification requires careful testing
Human-readable display is valuable
_format_int_value) makes debugging easier__repr__ showing properties helps users understand ParameterSet stateDuplicate detection is critical after refactoring
.py after major notebook editsgrep -c "def method_name" file.pyself.attributes_names β AttributeError (property has no setter)_is_com definition β NameErrorHwpObjectRun() with parameter sets_oleobj_ attributeCurrent State:
After Priority 2 Simplification:
This section compares the official HWP Automation Object Model (from HwpAutomation_2504.pdf) with the current hwpapi implementation to identify gaps, misalignments, and opportunities for better code organization.
Documentation Source:
hwp_docs/HwpAutomation_2504.pdf (Korean, dated 2025-04-15)
The official HWP automation follows a hierarchical object model similar to Microsoft Office:
IHwpObject (Root COM Object) β βββ IXHwpDocuments (Collection) β βββ IXHwpDocument (Single) β βββ Properties: FullName, Name, Path, Saved, etc. β βββ Methods: Save(), SaveAs(), Close(), Print(), etc. β βββ IXHwpWindows (Collection) β βββ IXHwpWindow (Single) β βββ Properties: Width, Height, Left, Top, Active, etc. β βββ Methods: Activate(), Close(), etc. β βββ IXHwpForms (Collection) β βββ Form Controls (Various types) β βββ IXHwpFormPushButtons (Collection) β βββ IXHwpFormCheckButtons (Collection) β βββ IXHwpFormRadioButtons (Collection) β βββ IXHwpFormComboBoxes (Collection) β βββ etc. β βββ HAction (Action Execution System) β βββ GetActionIDByName(name) β ActionID β βββ Run(ActionID) β βββ Execute(ActionID, ParameterSet) β βββ HParameterSet (Parameter Management) β βββ CreateItemSet(SetID, ParamIndex) β Creates nested parameter set β βββ Item(ParamIndex) β Get parameter value β βββ SetItem(ParamIndex, Value) β Set parameter value β βββ Clear() β Clear all parameters β βββ HSet (Parameter Collection - Legacy) β βββ Collection of parameters for complex actions β βββ HArray (Parameter Arrays - PIT_ARRAY type) βββ Count β Number of elements βββ Item(index) β Get element at index βββ SetItem(index, value) β Set element at index βββ Add(value) β Append element βββ RemoveAt(index) β Remove element at index
Key Characteristics:
The current implementation uses a wrapper-based approach with custom patterns:
App (Main Entry Point) β βββ Engine β βββ impl (HwpObject COM object) β βββ Direct COM access: self.api.MovePos(), self.api.Run(), etc. β βββ _Actions (900+ actions as properties) β βββ CharShape β _Action("CharShape", CharShape parameterset) β βββ ParaShape β _Action("ParaShape", ParaShape parameterset) β βββ [899+ more actions...] β βββ ParameterSet System (130+ classes in parametersets.py) β βββ Base: ParameterSet, ParameterSetMeta β βββ Backend Abstraction: β β βββ PsetBackend (modern, immediate) β β βββ HParamBackend (legacy, staging) β β βββ ComBackend (generic COM) β β βββ AttrBackend (pure Python) β βββ Property Descriptors: β β βββ IntProperty, BoolProperty, StringProperty β β βββ ColorProperty, UnitProperty β β βββ MappedProperty, TypedProperty, ListProperty β β βββ Auto-registration via ParameterSetMeta β βββ 130+ ParameterSet Subclasses: β βββ Text/Char: CharShape, ParaShape, BulletShape, etc. β βββ Tables: Table, Cell, TableCreation, etc. β βββ Drawing: ShapeObject, DrawLineAttr, DrawImageAttr, etc. β βββ Document: DocumentInfo, PageDef, SecDef, etc. β βββ [All mixed in single 3,357-line file] β βββ Custom Accessors (Pythonic convenience layer) β βββ MoveAccessor: Navigation (move.top_of_file(), move.bottom(), etc.) β βββ CellAccessor: Table cell operations β βββ TableAccessor: Table operations β βββ PageAccessor: Page operations β βββ Dataclasses (Alternative representation) βββ Character, CharShape (dataclass) βββ Paragraph, ParaShape (dataclass) βββ PageShape (dataclass)
Key Characteristics:
App object, no collections exposed_Actions| Aspect | Official HWP Model | Current hwpapi | Alignment |
|---|---|---|---|
| Entry Point | COM object | wrapper around | β Aligned (wrapped) |
| Document Access | collection | direct access | β Collection pattern not exposed |
| Window Management | collection | only | β οΈ Partial (no multi-window support) |
| Form Controls | collection | Not exposed | β Missing |
| Action Execution | | | β Aligned (pythonic wrapper) |
| Parameter Sets | COM object | Python classes | β Well abstracted |
| Parameter Typing | COM types | Python property descriptors | β Excellent (better than COM) |
| Nested Parameters | method | auto-creates | β Enhanced (auto-creating) |
| Arrays (HArray) | COM array methods | + | β Enhanced (Pythonic list) |
| Navigation | Object hierarchy | Custom accessors | β οΈ Different paradigm |
| Organization | Domain-based modules | Single monolithic file | β Poor organization |
Issue: hwpapi doesn't expose collection objects like
IXHwpDocuments, IXHwpWindows, IXHwpForms
Impact:
Example (What's Missing):
# This is possible in official HWP but not in hwpapi: documents = hwp.Documents # Collection of all open documents doc = documents.Item(0) # Get first document doc.Save() # Save specific document
Current hwpapi:
# Only single document access: app = App() # Always refers to "current" document app.save() # Saves "current" document only
Issue: All 130+ ParameterSet classes crammed into single 3,357-line file
Impact:
Breakdown:
parametersets.py (3,357 lines): βββ Mappings (147 lines): All DIRECTION_MAP, ALIGNMENT_MAP, etc. βββ Backend System (350 lines): Protocols, backend classes βββ Property Descriptors (250 lines): IntProperty, BoolProperty, etc. βββ ParameterSet Base (150 lines): Base class, metaclass βββ 130+ ParameterSet Classes (2,460 lines): βββ Text/Character (15 classes): CharShape, ParaShape, BulletShape, etc. βββ Tables (12 classes): Table, Cell, TableCreation, etc. βββ Drawing/Shapes (25 classes): ShapeObject, DrawLineAttr, etc. βββ Document (18 classes): DocumentInfo, PageDef, SecDef, etc. βββ Find/Replace (5 classes): FindReplace, DocFindInfo, etc. βββ Forms (8 classes): AutoFill, AutoNum, FieldCtrl, etc. βββ Formatting (12 classes): BorderFill, Caption, DropCap, etc. βββ Actions (15 classes): FileOpen, FileSaveAs, Print, etc. βββ Misc (20 classes): Everything else
Issue: Official model uses object hierarchy, hwpapi uses position-based accessors
Official Model (Object-Based):
# Hypothetical object-based navigation: document = app.ActiveDocument section = document.Sections[0] paragraph = section.Paragraphs[5] text = paragraph.Text
Current hwpapi (Position-Based):
# Position-based navigation: app.move.current_list(para=5, pos=0) app.actions.CharShape(...)
Analysis: Current approach is more pragmatic for HWP's position-based model. No change needed.
Issue: No access to IXHwpForms, form button controls, etc.
Impact:
Goal: Split monolithic
parametersets.py into domain-based submodules
New Structure:
hwpapi/ βββ parametersets/ β βββ __init__.py # Re-export all classes for compatibility β βββ base.py # ParameterSet base class, metaclass β βββ backends.py # Backend protocol, implementations β βββ properties.py # Property descriptors β βββ mappings.py # All DIRECTION_MAP, ALIGNMENT_MAP, etc. β βββ text/ β β βββ __init__.py β β βββ character.py # CharShape, BulletShape β β βββ paragraph.py # ParaShape, TabDef, ListProperties β β βββ numbering.py # NumberingShape, AutoNum β βββ table/ β β βββ __init__.py β β βββ table.py # Table, TableCreation β β βββ cell.py # Cell, CellBorderFill β βββ drawing/ β β βββ __init__.py β β βββ shape.py # ShapeObject, DrawLayout β β βββ line.py # DrawLineAttr β β βββ image.py # DrawImageAttr, DrawImageScissoring β β βββ effects.py # DrawShadow, DrawRotate, DrawTextart β βββ document/ β β βββ __init__.py β β βββ info.py # DocumentInfo, SummaryInfo, VersionInfo β β βββ page.py # PageDef, PageBorderFill, MasterPage β β βββ section.py # SecDef, ColDef β βββ formatting/ β β βββ __init__.py β β βββ border.py # BorderFill, BorderFillExt β β βββ caption.py # Caption, FootnoteShape β β βββ style.py # Style, StyleTemplate β βββ actions/ β β βββ __init__.py β β βββ file.py # FileOpen, FileSaveAs, FileConvert β β βββ edit.py # FindReplace, ConvertCase, etc. β β βββ print.py # Print, PrintToImage, PrintWatermark β βββ forms/ β βββ __init__.py β βββ fields.py # AutoFill, FieldCtrl, HyperLink
Benefits:
text/character.py, not line 850 of monolith__init__.py preserves existing importsMigration:
# Old import (still works): from hwpapi.parametersets import CharShape, ParaShape, Table # New import (also works): from hwpapi.parametersets.text.character import CharShape from hwpapi.parametersets.text.paragraph import ParaShape from hwpapi.parametersets.table.table import Table
Estimated Impact:
Goal: Expose
Documents, Windows collections to match official API
New API:
# Add to App class: class App: @property def documents(self): """Access to IXHwpDocuments collection.""" return DocumentsCollection(self.api) @property def windows(self): """Access to IXHwpWindows collection.""" return WindowsCollection(self.api) @property def active_document(self): """Currently active document.""" return Document(self.api.ActiveDocument) # New collection classes: class DocumentsCollection: def __init__(self, hwp_object): self._hwp = hwp_object def __len__(self): return self._hwp.Documents.Count def __getitem__(self, index): return Document(self._hwp.Documents.Item(index)) def add(self): """Create new document.""" return Document(self._hwp.Documents.Add()) class Document: def __init__(self, doc_com_object): self._doc = doc_com_object @property def full_name(self): return self._doc.FullName def save(self): return self._doc.Save() def close(self): return self._doc.Close()
Usage:
app = App() # Access collections: print(f"Open documents: {len(app.documents)}") doc1 = app.documents[0] doc2 = app.documents[1] # Multi-document workflows: for doc in app.documents: print(doc.full_name) doc.save() # Create new document: new_doc = app.documents.add()
Benefits:
Goal: Expose form controls for interactive documents
New Classes:
# Add to App: class App: @property def forms(self): """Access to form controls.""" return FormsCollection(self.api) class FormsCollection: def __init__(self, hwp_object): self._hwp = hwp_object @property def push_buttons(self): return PushButtonsCollection(self._hwp.Forms.PushButtons) @property def check_buttons(self): return CheckButtonsCollection(self._hwp.Forms.CheckButtons) # etc...
Benefits:
| Priority | Task | Lines Saved | Complexity Reduction | User Impact |
|---|---|---|---|---|
| 1 | Split parametersets.py by domain | 0 (reorg) | πΌπΌπΌ High | Low (internal) |
| 2 | Unify backend modes | ~200 | πΌπΌ Medium | Low (internal) |
| 3 | Expose Documents/Windows collections | +150 | π½ Slight increase | πΌπΌ High (feature) |
| 4 | Consolidate property types | ~200 | πΌ Medium | Low (internal) |
| 5 | Add Form controls support | +200 | π½ Slight increase | πΌ Medium (feature) |
| 6 | Remove forward declarations | ~25 | πΌ Small | None |
Recommendation: Start with Priority 1 (split parametersets.py) as it has:
Create directory structure:
mkdir -p hwpapi/parametersets/{text,table,drawing,document,formatting,actions,forms}
Create
__init__.py files with re-exports:
# hwpapi/parametersets/__init__.py from .base import ParameterSet, ParameterSetMeta from .backends import * from .properties import * from .text.character import CharShape, BulletShape from .text.paragraph import ParaShape, TabDef # ... (re-export all classes to preserve imports) __all__ = ['ParameterSet', 'CharShape', 'ParaShape', ...] # Full list
Move classes to domain files:
text/character.pytext/paragraph.pyUpdate notebook:
nbs/02_api/02_parameters.ipynb into multiple notebooks:
02_parameters_base.ipynb β base.py02_parameters_text_char.ipynb β text/character.pyTest imports:
# Ensure backward compatibility: from hwpapi.parametersets import CharShape # Should still work
Immediate Benefits (Phase 1 - Reorganization):
Long-term Benefits (Phase 2-3 - New Features):
Non-Goals:
When implementing the restructuring:
Preparation:
Phase 1 Execution:
hwpapi/parametersets/ package structurebase.pybackends.pyproperties.pymappings.py__init__.py with full re-exportsnbdev_exportpython -c "from hwpapi.parametersets import CharShape, ParaShape, Table"python -m pytest tests/ -vPhase 2 Execution (Optional):
app.documents, app.windows propertiesPhase 3 Execution (Optional):
app.forms property2025-12-09 - Logging System Improvements
INFO to WARNING
HWPAPI_LOG_LEVEL=DEBUG or INFOHWPAPI_LOG_LEVEL now defaults to WARNING instead of INFOnbs/02_api/06_logging.ipynb, hwpapi/logging.py2025-12-09 - Complete Display Enhancement Suite
Critical Bug Fix: Removed duplicate ParameterSet class definition in cell 26
_format_int_value overrode first with old logicFontSize to show as 1200 instead of 12.0ptEnhancement 1: Human-Readable Value Formatting
0x0000FF β #FF0000 (BBGGRR to hex)1200 β 12.0pt (HWPUNIT to pt)59430 β 210.0mm (HWPUNIT to mm)True/FalseEnhancement 2: Enum Display for MappedProperty
Direction="down"Direction=0 (down) (shows both value and name)Type=0 (μΌλ°μ±
κ°νΌ)Enhancement 3: Property Description Comments
FontSize=12.0ptFontSize=12.0pt # Font size in HWPUNIT (100 = 1pt)Complete Example:
CharFormat( FontSize=12.0pt # Font size in HWPUNIT (100 = 1pt) TextColor="#ff0000" # Text color in BBGGRR format Direction=0 (down) # Search direction (down=forward, up=backward) )
Detection Tools: Added scripts to detect duplicate method definitions
Documentation: Updated CLAUDE.md with Issue 5, debugging tips, examples
Result: Self-documenting, human-readable parameter display
Files:
nbs/02_api/02_parameters.ipynb, hwpapi/parametersets.py
Examples:
nested_property_demo.py, mapped_property_display_demo.py, property_description_display_demo.py
2025-01-08 - Auto-Creating Properties Design
NestedProperty for auto-creating nested ParameterSetsArrayProperty for Pythonic HArray interfaceUnitProperty for smart unit conversion (mm, cm, in, pt β HWPUNIT)2025-01-08 - Architecture Analysis & Restructuring Plan
2024 - Auto-Generated attributes_names
self.attributes_names = [...] from 9+ classes@property attributes_names to ParameterSet_del_value to handle None backend2024 - Added _is_com Function
_looks_like_psetEarlier - Pset Migration
Before making changes:
nbdev_export successfullyBefore committing:
nbdev_exportpython -m pytest tests/ -v)# Export notebooks to Python nbdev_export # Run all tests python -m pytest tests/ -v # Test specific module python -m pytest tests/test_hparam.py -v # Verify imports python -c "import hwpapi; print('OK')" # Check notebook validity python -m nbformat.validate nbs/02_api/02_parameters.ipynb # Generate documentation nbdev_docs # Clean generated files nbdev_clean # Preview documentation nbdev_preview
make_backend() factoryattributes_names neededThis document is a living guide. Update it as you learn more about the codebase.
Last Updated: 2025-12-09 (After fixing duplicate class and display formatting) Next Review: After Phase 1 restructuring (split parametersets.py by domain)