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
8 changes: 6 additions & 2 deletions .github/workflows/codespell.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ on:
push:
branches: [main]
pull_request:
branches: [main]
types:
- opened
- reopened
- synchronize
- ready_for_review

permissions:
contents: read
Expand All @@ -18,6 +22,6 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Codespell
uses: codespell-project/actions-codespell@v2
2 changes: 1 addition & 1 deletion .github/workflows/deploy_documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
- name: Set up Python3
uses: actions/setup-python@v5
with:
python-version: 3.9
python-version: 3.12
cache: 'poetry'

- name: Install dependencies
Expand Down
13 changes: 9 additions & 4 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ name: Build and test
on:
push:
branches: [ main ]

pull_request:
branches: [ main ]
types:
- opened
- reopened
- synchronize
- ready_for_review
Comment on lines +12 to +16
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

run linting and test on all PRs in a non-draft state, regardless of what the base branch is


jobs:
lint:
Expand All @@ -25,14 +30,14 @@ jobs:
run: |
pip install tox

- name: Check code quality with flake8
run: tox -e flake8
- name: Check code quality with ruff
run: tox -e lint_stats

test:
runs-on: ubuntu-latest
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Below, also inject poetry dynamic versioning into pipx-installed poetry:

  - name: Install Poetry
      run: |
        pipx install poetry
        pipx inject poetry poetry-dynamic-versioning

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Versioning isn't really needed to run the tests, is it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the run logs a version 0.0.0 is installed. While it does not break tests, you will also not notice if dynamic versioning breaks or is configured wrong, if version 0.0.0 is "good enough".

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO it'd be better to have an explicit test that ensures that versioning is working as expected, which will error out if the versioning plugin hasn't done its job. It can be very simple -- e.g. grepping the version info from one of the places where the plugin automatically updates it, and ensuring that it isn't 0.0.0. I don't want to have to check run logs unless I'm looking for reasons why tests failed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's worth adding such a workflow or test because it's something that can easily be checked the next time that a release is made, and having a workflow that tests the functionality as part of the linkml-map test suite is essentially testing other peoples' code.

strategy:
matrix:
python-version: [ '3.9', '3.10', '3.11', '3.12' ]
python-version: [ '3.9', '3.10', '3.11', '3.12', '3.13' ]

steps:
- uses: actions/checkout@v4
Expand Down
1,576 changes: 946 additions & 630 deletions poetry.lock

Large diffs are not rendered by default.

209 changes: 164 additions & 45 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,50 +1,65 @@
[tool.poetry]
[project]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

switched this over to using a more generic format, which will make the transition to using uv, if such a transition occurs, smoother.

name = "linkml-map"
version = "0.0.0"
description = "a framework for specifying and executing mappings between data models"
authors = ["cmungall <cjm@berkeleybop.org>"]
readme = "README.md"
authors = [
{name = "cmungall", email = "cjm@berkeleybop.org"},
]
requires-python = "<=3.13,>=3.9"
dynamic = [ "version" ]

dependencies = [
"asteval<1,>=0",
"click<9,>=8",
"curies",
"jinja2<4,>=3",
"linkml-runtime>=1.7.2",
"pydantic>=2.0.0",
"pyyaml",
]

[project.optional-dependencies]
duckdb = [
"duckdb<1,>=0",
]
graphviz = [
"graphviz<1.0.0,>=0.20.1",
]
units = [
"lark<2,>=1",
"pint<1,>=0",
"ucumvert<1,>=0",
]
dev = [
"deepdiff<7.0.0,>=6.7.1",
"deptry<1.0.0,>=0.21.1",
"jupyter<2.0.0,>=1.0.0",
"linkml>=1.7.0",
"mkdocs-mermaid2-plugin<1.0.0,>=0.6.0",
"mkdocs-windmill",
"mkdocstrings[crystal,python]",
"mknotebooks<1.0.0,>=0.8.0",
"pytest>=7.3.1",
"ruff>=0.11.2",
"tox>=4.24.2",
]
Comment on lines +34 to +46
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious about this dev group. As I understand it, the project.optional-dependencies section is used to define package extras. So there's two implications here:

  • A downstream client could theoretically do pip install linkml-map[dev]. There's probably no valid reason for them to do that, but it's possible and maybe could cause confusion?
  • A developer of this package needs to remember to run poetry install -E dev (or --all-extras) to get the development dependencies.

uv supports the dependency-groups section for development dependencies. However Poetry does not yet. They still recommend using a tool.poetry.group.<group> section.

With all that said, what do you think about moving these dependencies back to tool.poetry.group.dev.dependencies for now? I know that hampers having this be a Poetry- and uv-compatible project file, but it keeps us inline with current recommended practices. And it seems like it would be an easy switch later when Poetry supports dependency-groups. Let me know what you think.

Copy link
Collaborator Author

@ialarmedalien ialarmedalien Apr 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm planning to switch this repo to using uv in the next PR. Since this is a relatively low traffic repo (vs linkml-runtime or linkml), I think it should be safe just to switch. @sierra-moxon does that sound OK to you?

I can easily either double up the requirements sections (so there are temporarily both dependency-groups and tool.poetry.group.<group> sections) or comment out the new-fangled dependency-groups and restore the poetry section headers in the meantime.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm planning to switch this repo to using uv in the next PR.

Ah okay. In that case, if this is just a stepping stone and then next PR will convert to using dependency-groups in short order then I'm less concerned.


[project.scripts]
linkml-map = "linkml_map.cli.cli:main"

[tool.poetry.dependencies]
python = "^3.9"
asteval = "^0"
click = "^8"
curies = "*"
duckdb = { version = "^0", optional = true }
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to optional-dependencies section

graphviz = { version = "^0.20.1", optional = true }
jinja2 = "^3"
lark = { version = "^1", optional = true }
linkml-runtime = ">=1.7.2"
pint = { version = "^0", optional = true }
pydantic = ">=2.0.0"
pyyaml = "*"
ucumvert = { version = "^0", optional = true }

[tool.poetry.extras]
duckdb = ["duckdb"]
graphviz = ["graphviz"]
units = ["lark", "pint", "ucumvert"]

[tool.poetry.group.dev.dependencies]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is also in the optional-dependencies section now

deepdiff = "^6.7.1"
deptry = "^0.21.1"
jupyter = "^1.0.0"
linkml = ">=1.7.0"
mkdocs-mermaid2-plugin = "^0.6.0"
mkdocs-windmill = "*"
mkdocstrings = {extras = ["crystal", "python"], version = "*"}
mknotebooks = "^0.8.0"
pytest = "^7.3.1"
tox = "*"
[tool.poetry]
requires-poetry = ">=2.0"
version = "0.0.0"

[tool.poetry.requires-plugins]
poetry-dynamic-versioning = { version = ">=1.0.0,<2.0.0", extras = ["plugin"] }

[tool.poetry-dynamic-versioning]
enable = true
vcs = "git"
style = "pep440"

[tool.poetry.scripts]
linkml-map = "linkml_map.cli.cli:main"

[tool.deptry]
known_first_party = ["linkml_map"]
extend_exclude = ["docs"]
Expand All @@ -53,19 +68,123 @@ extend_exclude = ["docs"]
requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning"]
build-backend = "poetry_dynamic_versioning.backend"

[tool.black]
line-length = 100
target-version = ["py39", "py310", "py311"]

[tool.isort]
profile = "black"
multi_line_output = 3
include_trailing_comma = true
reverse_relative = true

# Ref: https://github.com/codespell-project/codespell#using-a-config-file
[tool.codespell]
skip = '.git,*.pdf,*.svg,go.sum,*.lock'
check-hidden = true
ignore-regex = '(^\s*"image/\S+": ".*|\b(KEGG.BRITE|mor.nlm.nih.gov)\b)'
ignore-words-list = 'infarction,amination,assertIn,brite,ehr,mor,brite,nin,mirgate'

[tool.ruff]
line-length = 100
target-version = "py312"

[tool.ruff.format]
exclude = [
"docs/",
"src/linkml_map/datamodel",
"tests/input"
]

[tool.ruff.lint]
exclude = [
"docs/",
"src/linkml_map/datamodel",
"tests/input"
]

select = [
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This list is all the linter extensions currently available for ruff. ATM most of these are active so the linter throws up a lot of errors; these will be fixed in an upcoming PR. Not all the linter extensions are useful or desired but I find it easier to have the list in the pyproject file rather than having to go to the ruff website and work out what you want / don't want there.

# core
"F", # Pyflakes
"E", # pycodestyle errors
"W", # pycodestyle warnings
"C90", # mccabe +
"I", # isort
"N", # pep8-naming
"D", # pydocstyle
"UP", # pyupgrade
# extensions
"YTT", # flake8-2020
"ANN", # flake8-annotations
"ASYNC", # flake8-async
"S", # flake8-bandit
"BLE", # flake8-blind-except
"FBT", # flake8-boolean-trap
"B", # flake8-bugbear
"A", # flake8-builtins
# "COM", # flake8-commas
# "CPY", # flake8-copyright
"C4", # flake8-comprehensions
"DTZ", # flake8-datetimez
"T10", # flake8-debugger
# "DJ", # flake8-django
"EM", # flake8-errmsg
"EXE", # flake8-executable
"FA", # flake8-future-annotations
"ISC", # flake8-implicit-str-concat
"ICN", # flake8-import-conventions
"G", # flake8-logging-format
"INP", # flake8-no-pep420
"PIE", # flake8-pie
"T20", # flake8-print
"PYI", # flake8-pyi
"PT", # flake8-pytest-style
"Q", # flake8-quotes
"RSE", # flake8-raise
"RET", # flake8-return
"SLF", # flake8-self
"SLOT", # flake8-slots
"SIM", # flake8-simplify
"TID", # flake8-tidy-imports
"TCH", # flake8-type-checking
"INT", # flake8-gettext
"ARG", # flake8-unused-arguments
# "PTH", # flake8-use-pathlib
# "TD", # flake8-todos
# "FIX", # flake8-fixme
"ERA", # eradicate
"PD", # pandas-vet
"PGH", # pygrep-hooks
"PL", # Pylint
"TRY", # tryceratops
"FLY", # flynt
"NPY", # NumPy-specific rules
"AIR", # Airflow
"PERF", # Perflint
"FURB", # refurb
"LOG", # flake8-logging
"RUF", # Ruff-specific rules
]

ignore = [
# UP007: non-pep604-annotation-union - fix when 3.9 is dropped
"UP007",
# D200: unnecessary-multiline-docstring
"D200",
# D203: one-blank-line-before-class (conflicts with D211)
"D203",
# D212: multi-line-summary-first-line (conflicts with D213)
"D212",
# E203: whitespace before ',', ';', or ':'
"E203",
# E501: line length (specified elsewhere)
"E501",
# ISC001: conflicts with Ruff's formatter
"ISC001",
# C901: complex-structure - needs code change
"C901",
# E731: lambda-assignment - needs code change
"E731",
# B024: abstract-base-class-without-abstract-method
"B024",
# T201: print
"T201"
]

[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S101"] # use of assert

[tool.ruff.lint.mccabe]
# Flag errors (`C901`) whenever the complexity level exceeds 15.
max-complexity = 15
5 changes: 2 additions & 3 deletions src/linkml_map/compiler/graphviz_compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ class GraphvizObject(CompiledSpecification):
digraph: Digraph = None

def render(self, file_path: str, format="png", view=False) -> None:
"""
Renders a graphviz graph to a file.
"""Render a graphviz graph to a file.
:param file_path:
:return:
"""
Expand All @@ -55,7 +54,7 @@ def render(self, file_path: str, format="png", view=False) -> None:

class GraphvizCompiler(Compiler):
"""
Compiles a Transformation Specification to GraphViz.
Compile a Transformation Specification to GraphViz.
"""

def compile(
Expand Down
29 changes: 14 additions & 15 deletions src/linkml_map/transformer/object_transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from linkml_map.utils.dynamic_object import DynObj, dynamic_object
from linkml_map.utils.eval_utils import eval_expr, eval_expr_with_mapping

DICT_OBJ = Dict[str, Any]
DICT_OBJ = dict[str, Any]


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -297,7 +297,11 @@ def map_object(
return tgt_attrs

def _perform_unit_conversion(
self, slot_derivation: SlotDerivation, source_obj: Any, sv: SchemaView, source_type: str
self,
slot_derivation: SlotDerivation,
source_obj: Any,
sv: SchemaView,
source_type: str,
) -> Union[float, Dict]:
uc = slot_derivation.unit_conversion
curr_v = source_obj.get(slot_derivation.populated_from, None)
Expand All @@ -309,13 +313,12 @@ def _perform_unit_conversion(
from_unit = curr_v.get(uc.source_unit_slot, None)
if from_unit is None:
raise ValueError(
f"Could not determine unit from {curr_v}" f" using {uc.source_unit_slot}"
f"Could not determine unit from {curr_v} using {uc.source_unit_slot}"
)
magnitude = curr_v.get(uc.source_magnitude_slot, None)
if magnitude is None:
raise ValueError(
f"Could not determine magnitude from {curr_v}"
f" using {uc.source_magnitude_slot}"
f"Could not determine magnitude from {curr_v} using {uc.source_magnitude_slot}"
)
else:
if slot.unit.ucum_code:
Expand Down Expand Up @@ -358,21 +361,18 @@ def _multivalued_to_singlevalued(self, vs: List[Any], slot_derivation: SlotDeriv
delimiter = stringification.delimiter
if delimiter:
return delimiter.join(vs)
elif stringification.syntax:
if stringification.syntax:
if stringification.syntax == SerializationSyntaxType.JSON:
return json.dumps(vs)
elif stringification.syntax == SerializationSyntaxType.YAML:
if stringification.syntax == SerializationSyntaxType.YAML:
return yaml.dump(vs, default_flow_style=True).strip()
else:
raise ValueError(f"Unknown syntax: {stringification.syntax}")
else:
raise ValueError(f"Cannot convert multivalued to single valued: {vs}; no delimiter")
raise ValueError(f"Unknown syntax: {stringification.syntax}")
raise ValueError(f"Cannot convert multivalued to single valued: {vs}; no delimiter")
if len(vs) > 1:
raise ValueError(f"Cannot coerce multiple values {vs}")
if len(vs) == 0:
return None
else:
return vs[0]
return vs[0]

def _singlevalued_to_multivalued(self, v: Any, slot_derivation: SlotDerivation) -> List[Any]:
stringification = slot_derivation.stringification
Expand Down Expand Up @@ -443,5 +443,4 @@ def transform_enum(self, source_value: str, enum_name: str, source_obj: Any) ->
return pv_deriv.name
if enum_deriv.mirror_source:
return str(source_value)
else:
return None
return None
Loading