Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 42 additions & 4 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
name: Publish to PyPI
name: Release

# Publishes a release to PyPI using OIDC trusted publishing (no API tokens)
# and creates a matching GitHub Release. Triggered by pushing a version tag,
# e.g. `git tag v0.2.3 && git push origin v0.2.3`.
#
# Required one-time configuration (repository settings):
# - PyPI trusted publisher: project `glazing`, owner `factslab`,
# repository `glazing`, workflow `publish.yml`, environment `pypi`.
# - GitHub environment `pypi` (optionally with required reviewers / tag
# protection rules) that gates the publish job.

on:
push:
Expand All @@ -23,22 +33,27 @@ jobs:
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
pip install build
pip install build twine

- name: Build distribution
run: python -m build

- name: Check distribution
run: twine check dist/*

- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/

publish:
publish-pypi:
name: Publish to PyPI
needs: build
runs-on: ubuntu-latest
environment: pypi
environment:
name: pypi
url: https://pypi.org/p/glazing
permissions:
id-token: write
steps:
Expand All @@ -50,3 +65,26 @@ jobs:

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

github-release:
name: GitHub Release
needs: publish-pypi
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: dist/

- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: >-
gh release create "${{ github.ref_name }}"
--repo "${{ github.repository }}"
--title "${{ github.ref_name }}"
--generate-notes
dist/*
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.2.3] - 2026-06-25

### Added

- **WordNet adjective syntactic-position markers** (`p` predicative, `a` attributive, `ip` immediately postnominal) are now parsed off adjective words into the new `Word.syntactic_marker` field (#8)

### Fixed

- **WordNet synset `lex_filename`** now derives from the canonical WordNet `lexnames` lexicographer-file table; previously every entry from index 2 onward was shifted, so e.g. `lex_filenum=29` (`verb.body`) was mislabeled `noun.time` and `lex_filenum=3` (`noun.Tops`) was mislabeled `adv.all` (#9)
- **WordNet adjective synsets carrying a syntactic marker** are no longer silently dropped during conversion; the parenthesized marker previously stayed on the lemma, failed lemma validation, and discarded the entire synset (#8)
- **FrameNet FE Requires/Excludes constraints** now populate `FrameElement.requires_fe` and `FrameElement.excludes_fe`; the converter previously read the (nonexistent) child elements of the first `<requiresFE>`/`<excludesFE>` tag rather than the `name` attribute of each sibling, so the lists were always empty (#7)

## [0.2.2] - 2026-02-06

### Added
Expand Down
2 changes: 1 addition & 1 deletion docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ except ValidationError as e:

## Version Compatibility

This documentation covers Glazing version 0.2.2. Check your installed version:
This documentation covers glazing version 0.2.3. Check your installed version:

```python
import glazing
Expand Down
8 changes: 4 additions & 4 deletions docs/citation.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,22 @@ If you use Glazing in your research, please cite our work.
title = {Glazing: Unified Data Models and Interfaces for Syntactic and Semantic Frame Ontologies},
year = {2025},
url = {https://github.com/factslab/glazing},
version = {0.2.2},
version = {0.2.3},
doi = {10.5281/zenodo.17467082}
}
```

### APA

White, A. S. (2025). *Glazing: Unified Data Models and Interfaces for Syntactic and Semantic Frame Ontologies* (Version 0.2.2) [Computer software]. https://github.com/factslab/glazing
White, A. S. (2025). *Glazing: Unified Data Models and Interfaces for Syntactic and Semantic Frame Ontologies* (Version 0.2.3) [Computer software]. https://github.com/factslab/glazing

### Chicago

White, Aaron Steven. 2025. *Glazing: Unified Data Models and Interfaces for Syntactic and Semantic Frame Ontologies*. Version 0.2.2. https://github.com/factslab/glazing.
White, Aaron Steven. 2025. *Glazing: Unified Data Models and Interfaces for Syntactic and Semantic Frame Ontologies*. Version 0.2.3. https://github.com/factslab/glazing.

### MLA

White, Aaron Steven. *Glazing: Unified Data Models and Interfaces for Syntactic and Semantic Frame Ontologies*. Version 0.2.2, 2025, https://github.com/factslab/glazing.
White, Aaron Steven. *Glazing: Unified Data Models and Interfaces for Syntactic and Semantic Frame Ontologies*. Version 0.2.3, 2025, https://github.com/factslab/glazing.

## Citing Datasets

Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ If you use Glazing in your research, please cite:
title = {Glazing: Unified Data Models and Interfaces for Syntactic and Semantic Frame Ontologies},
year = {2025},
url = {https://github.com/factslab/glazing},
version = {0.2.2},
version = {0.2.3},
doi = {10.5281/zenodo.17467082}
}
```
Expand Down
7 changes: 5 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "glazing"
version = "0.2.2"
version = "0.2.3"
description = "Unified data models and interfaces for syntactic and semantic frame ontologies"
readme = "README.md"
requires-python = ">=3.13"
Expand Down Expand Up @@ -38,7 +38,10 @@ dependencies = [
[project.optional-dependencies]
dev = [
"ruff>=0.1.9",
"mypy>=1.8.0",
# Pinned: --strict behavior changes between minor releases, so an unpinned
# spec lets CI and local environments drift (and CI's Type Check fail on
# code that passes locally). Bump deliberately.
"mypy==2.1.0",
"pytest>=7.4.3",
"pytest-cov>=4.1.0",
"pytest-asyncio>=0.21.1",
Expand Down
2 changes: 1 addition & 1 deletion src/glazing/__version__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Version information for the glazing package."""

__version__ = "0.2.2"
__version__ = "0.2.3"
__version_info__ = tuple(int(i) for i in __version__.split("."))
14 changes: 6 additions & 8 deletions src/glazing/downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,17 +617,15 @@ def get_downloader(dataset: DatasetType | str) -> BaseDownloader:
>>> print(downloader.version)
3.4
"""
# Normalize to lowercase for case-insensitive lookup
dataset_lower = dataset.lower()

if dataset_lower not in _DOWNLOADERS:
# Normalize to lowercase for case-insensitive lookup. The cast narrows the
# plain ``str`` to the key type for the lookup; ``.get`` still returns None
# for any unsupported name, so the cast never asserts a false membership.
dataset_lower = cast(DatasetType, dataset.lower())
downloader_class = _DOWNLOADERS.get(dataset_lower)
if downloader_class is None:
supported = ", ".join(_DOWNLOADERS.keys())
msg = f"Unsupported dataset: {dataset}. Supported: {supported}"
raise ValueError(msg)

# Cast to DatasetType for type checking
dataset_typed = cast(DatasetType, dataset_lower)
downloader_class = _DOWNLOADERS[dataset_typed]
return downloader_class()


Expand Down
24 changes: 10 additions & 14 deletions src/glazing/framenet/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,20 +274,16 @@ def _parse_fe_constraints(self, fe_elem: etree._Element) -> tuple[list[str], lis
tuple[list[str], list[str]]
Requires and excludes lists.
"""
requires_fe = []
excludes_fe = []

requires_elem = fe_elem.find(
f"{{{self.namespace}}}requiresFE" if self.namespace else "requiresFE"
)
if requires_elem is not None:
requires_fe = [req.get("name", "") for req in requires_elem]

excludes_elem = fe_elem.find(
f"{{{self.namespace}}}excludesFE" if self.namespace else "excludesFE"
)
if excludes_elem is not None:
excludes_fe = [exc.get("name", "") for exc in excludes_elem]
# FrameNet frame XML carries each constraint as a self-closing sibling
# of <FE>, e.g. <requiresFE name="Partner_2" ID="488"/>. There can be
# several of each, and the target FE name lives in the ``name``
# attribute, not in a child element. Collect every matching sibling.
requires_fe = [
name for req in fe_elem.findall(self._tag("requiresFE")) if (name := req.get("name"))
]
excludes_fe = [
name for exc in fe_elem.findall(self._tag("excludesFE")) if (name := exc.get("name"))
]

return requires_fe, excludes_fe

Expand Down
125 changes: 79 additions & 46 deletions src/glazing/wordnet/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,17 @@
Word,
)
from glazing.wordnet.types import (
AdjPosition,
LexFileName,
PointerSymbol,
VerbFrameNumber,
WordNetPOS,
)

# Adjective syntactic-position markers appended to words in adjective data files,
# e.g. "galore(ip)" -> ("galore", "ip"). See wndb(5) / wninput(5).
ADJ_MARKER_RE = re.compile(r"^(?P<lemma>.+)\((?P<marker>ip|a|p)\)$")


class WordNetConverter:
"""Parse WordNet database files into structured models.
Expand All @@ -77,53 +82,57 @@ class WordNetConverter:
Convert entire WordNet database to JSON Lines.
"""

# Mapping from lexical file numbers to names
# Mapping from lexical file numbers to names.
#
# This follows the canonical WordNet ``lexnames`` lexicographer-file table
# (see wndb(5)). Note that ``adv.all`` is index 2 and ``adj.ppl`` is the
# final entry at index 44 -- the order is *not* a simple POS grouping.
LEX_FILE_NAMES: ClassVar[dict[int, LexFileName]] = {
0: "adj.all",
1: "adj.pert",
2: "adj.ppl",
3: "adv.all",
4: "noun.Tops",
5: "noun.act",
6: "noun.animal",
7: "noun.artifact",
8: "noun.attribute",
9: "noun.body",
10: "noun.cognition",
11: "noun.communication",
12: "noun.event",
13: "noun.feeling",
14: "noun.food",
15: "noun.group",
16: "noun.location",
17: "noun.motive",
18: "noun.object",
19: "noun.person",
20: "noun.phenomenon",
21: "noun.plant",
22: "noun.possession",
23: "noun.process",
24: "noun.quantity",
25: "noun.relation",
26: "noun.shape",
27: "noun.state",
28: "noun.substance",
29: "noun.time",
30: "verb.body",
31: "verb.change",
32: "verb.cognition",
33: "verb.communication",
34: "verb.competition",
35: "verb.consumption",
36: "verb.contact",
37: "verb.creation",
38: "verb.emotion",
39: "verb.motion",
40: "verb.perception",
41: "verb.possession",
42: "verb.social",
43: "verb.stative",
44: "verb.weather",
2: "adv.all",
3: "noun.Tops",
4: "noun.act",
5: "noun.animal",
6: "noun.artifact",
7: "noun.attribute",
8: "noun.body",
9: "noun.cognition",
10: "noun.communication",
11: "noun.event",
12: "noun.feeling",
13: "noun.food",
14: "noun.group",
15: "noun.location",
16: "noun.motive",
17: "noun.object",
18: "noun.person",
19: "noun.phenomenon",
20: "noun.plant",
21: "noun.possession",
22: "noun.process",
23: "noun.quantity",
24: "noun.relation",
25: "noun.shape",
26: "noun.state",
27: "noun.substance",
28: "noun.time",
29: "verb.body",
30: "verb.change",
31: "verb.cognition",
32: "verb.communication",
33: "verb.competition",
34: "verb.consumption",
35: "verb.contact",
36: "verb.creation",
37: "verb.emotion",
38: "verb.motion",
39: "verb.perception",
40: "verb.possession",
41: "verb.social",
42: "verb.stative",
43: "verb.weather",
44: "adj.ppl",
}

def parse_data_file(self, filepath: Path | str, pos: WordNetPOS) -> list[Synset]:
Expand Down Expand Up @@ -628,6 +637,30 @@ def convert_exceptions(self, wordnet_dir: Path | str, output_file: Path | str) -

return len(all_entries)

@staticmethod
def _split_adj_marker(word: str) -> tuple[str, AdjPosition | None]:
"""Split an adjective syntactic-position marker off a word token.

In WordNet adjective data files the syntactic marker is appended, in
parentheses, directly onto the word with no intervening space -- e.g.
``galore(ip)``, ``outback(a)``, ``ready_to_hand(p)``. See wndb(5).

Parameters
----------
word : str
Raw word token from a data file (possibly marker-bearing).

Returns
-------
tuple[str, AdjPosition | None]
The lemma with the marker stripped, and the marker (``p``/``a``/
``ip``) if present, otherwise ``None``.
"""
match = ADJ_MARKER_RE.match(word)
if match is None:
return word, None
return match.group("lemma"), cast(AdjPosition, match.group("marker"))

def _parse_data_line(self, line: str) -> Synset | None:
"""Parse a line from WordNet data file.

Expand Down Expand Up @@ -669,9 +702,9 @@ def _parse_data_line(self, line: str) -> Synset | None:
for _ in range(w_cnt):
if idx + 1 >= len(parts):
break
lemma = parts[idx]
lemma, marker = self._split_adj_marker(parts[idx])
lex_id = int(parts[idx + 1], 16)
words.append(Word(lemma=lemma, lex_id=lex_id))
words.append(Word(lemma=lemma, lex_id=lex_id, syntactic_marker=marker))
idx += 2

# Parse pointers
Expand Down
Loading
Loading