Skip to content

parse dependencies and add to header as #r entries#10

Merged
chenkasirer merged 2 commits intomainfrom
deps_to_r
Nov 12, 2025
Merged

parse dependencies and add to header as #r entries#10
chenkasirer merged 2 commits intomainfrom
deps_to_r

Conversation

@chenkasirer
Copy link
Copy Markdown
Member

--dev flag now also adds the package requirements to the header as #r entries.

This allows easy dev setups for development of GH components.

What type of change is this?

  • Bug fix in a backwards-compatible manner.
  • New feature in a backwards-compatible manner.
  • Breaking change: bug fix or new feature that involve incompatible API changes.
  • Other (e.g. doc update, configuration, etc)

Checklist

Put an x in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code.

  • I added a line to the CHANGELOG.md file in the Unreleased section under the most fitting heading (e.g. Added, Changed, Removed).
  • I ran all tests on my computer and it's all green (i.e. invoke test).
  • I ran lint on my computer and there are no errors (i.e. invoke lint).
  • I added new functions/classes and made them available on a second-level import, e.g. compas.datastructures.Mesh.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have added necessary documentation (if appropriate)

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR enhances the --dev flag functionality by parsing package dependencies from pyproject.toml or requirements files and adding them as #r entries in Grasshopper component headers, enabling easier development setups.

Key Changes:

  • Added dependency parsing logic from pyproject.toml (static and dynamic) and requirements files
  • Modified the update_gh_header function to include parsed dependencies when --dev flag is used
  • Updated CHANGELOG.md to document the new feature

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
src/compas_invocations2/grasshopper.py Added three new helper functions (_sanitize_dependency, _get_deps_from_requirements, _get_dependencies) to parse and sanitize dependencies, and integrated them into the update_gh_header function to add dependencies to the header when --dev is enabled
CHANGELOG.md Added entry documenting the new --dev flag enhancement

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

new_header.append(f"# env: {env.strip()}\n")
if dev:
dependencies = _get_dependencies(ctx.base_folder)
new_header.append(f"# r: {', '.join(dependencies)}\n")
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

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

If dependencies is an empty list, this will add an empty # r: line to the header, which might not be the intended behavior. Consider checking if the list is non-empty before adding it:

if dependencies:
    new_header.append(f"# r: {', '.join(dependencies)}\n")
Suggested change
new_header.append(f"# r: {', '.join(dependencies)}\n")
if dependencies:
new_header.append(f"# r: {', '.join(dependencies)}\n")

Copilot uses AI. Check for mistakes.
Comment on lines +86 to +117
def _sanitize_dependency(dep: str) -> str:
# Remove upper bound constraints (e.g., ", <3" or ",<3") as Rhino doesn't support them
sanitized = re.split(r"\s*,\s*[<>=]", dep)[0]
sanitized = sanitized.split("#")[0].strip() # Remove inline comments
return sanitized


def _get_deps_from_requirements(req_filepath: str) -> str:
with open(req_filepath, "r") as req_file:
dependencies = []
for line in req_file:
line = line.strip()
if line and not line.startswith("#"):
dependencies.append(_sanitize_dependency(line))
return dependencies


def _get_dependencies(base_folder: str) -> str:
toml_filepath = os.path.join(base_folder, "pyproject.toml")
with open(toml_filepath, "r") as f:
toml = tomlkit.load(f)

dependencies = toml.get("project").get("dependencies")
if dependencies:
return [_sanitize_dependency(dep) for dep in dependencies]

dynamic_deps = toml.get("tool").get("setuptools").get("dynamic").get("dependencies")
if dynamic_deps and "file" in dynamic_deps:
req_filepath = os.path.join(base_folder, dynamic_deps["file"])
return _get_deps_from_requirements(req_filepath)

return []
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

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

The new functions _sanitize_dependency, _get_deps_from_requirements, and _get_dependencies lack docstrings explaining their purpose, parameters, and return values. This makes it harder for other developers to understand their behavior. Consider adding docstrings following the project's documentation standards.

Copilot uses AI. Check for mistakes.
return sanitized


def _get_deps_from_requirements(req_filepath: str) -> str:
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

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

The return type annotation is incorrect. The function returns a list of strings, not a str. This should be -> list[str] or -> List[str] (with from typing import List).

Suggested change
def _get_deps_from_requirements(req_filepath: str) -> str:
def _get_deps_from_requirements(req_filepath: str) -> list[str]:

Copilot uses AI. Check for mistakes.
return dependencies


def _get_dependencies(base_folder: str) -> str:
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

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

The return type annotation is incorrect. The function returns a list of strings, not a str. This should be -> list[str] or -> List[str] (with from typing import List).

Suggested change
def _get_dependencies(base_folder: str) -> str:
def _get_dependencies(base_folder: str) -> list[str]:

Copilot uses AI. Check for mistakes.
Comment on lines +108 to +112
dependencies = toml.get("project").get("dependencies")
if dependencies:
return [_sanitize_dependency(dep) for dep in dependencies]

dynamic_deps = toml.get("tool").get("setuptools").get("dynamic").get("dependencies")
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

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

This code will raise an AttributeError if any of the nested keys don't exist. The .get() method returns None by default, and calling .get() on None will fail. Consider adding proper null checks or using a try-except block. For example:

dependencies = toml.get("project", {}).get("dependencies")

Or use a try-except to handle the case where the structure doesn't match expectations.

Suggested change
dependencies = toml.get("project").get("dependencies")
if dependencies:
return [_sanitize_dependency(dep) for dep in dependencies]
dynamic_deps = toml.get("tool").get("setuptools").get("dynamic").get("dependencies")
dependencies = toml.get("project", {}).get("dependencies")
if dependencies:
return [_sanitize_dependency(dep) for dep in dependencies]
dynamic_deps = toml.get("tool", {}).get("setuptools", {}).get("dynamic", {}).get("dependencies")

Copilot uses AI. Check for mistakes.
if dependencies:
return [_sanitize_dependency(dep) for dep in dependencies]

dynamic_deps = toml.get("tool").get("setuptools").get("dynamic").get("dependencies")
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

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

This code will raise an AttributeError if any of the nested keys don't exist. The chain of .get() calls returns None if any key is missing, and calling .get() on None will fail. Consider adding proper null checks or using a try-except block. For example:

dynamic_deps = toml.get("tool", {}).get("setuptools", {}).get("dynamic", {}).get("dependencies")
Suggested change
dynamic_deps = toml.get("tool").get("setuptools").get("dynamic").get("dependencies")
dynamic_deps = (
toml.get("tool", {})
.get("setuptools", {})
.get("dynamic", {})
.get("dependencies")
)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

@gonzalocasas gonzalocasas left a comment

Choose a reason for hiding this comment

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

Code looks good to me, but I haven't really understood what's the use-case this solves, in dev mode, I would I assume the components don't really need any # r: and one would have a custom python component running first in the GH file that has the # r: with all the locally relevant stuff, but I am surely missing something!

Also: Rhino doesn't support upper bounds? What sort of bizarre limit is that? Is it a problem with encoding because they are passing the <3 as piping from something and needs to be escaped or the like? Or is it an actual limit in their implementation?

@chenkasirer
Copy link
Copy Markdown
Member Author

Also: Rhino doesn't support upper bounds? What sort of bizarre limit is that? Is it a problem with encoding because they are passing the <3 as piping from something and needs to be escaped or the like? Or is it an actual limit in their implementation?

yes indead the comma is not escaped and this is what the resulting pip command looks like:

Info 11/12/2025 08:47:25 [RhinoCode] Installing "compas>=2.0.0, <3.0"
Info 11/12/2025 08:47:25 [RhinoCode] Running process: C:\Users\ckasirer\.rhinocode\py39-rh8\python.exe -I -m pip --isolated --disable-pip-version-check install --target "C:\Users\ckasirer\.rhinocode\py39-rh8\site-envs\default-QLMRNq0_" --progress-bar off --upgrade --no-warn-script-location --retries 5 --timeout 15 "compas>=2.0.0" "<3.0"
Error 11/12/2025 08:47:27 [RhinoCode] Process exited with code 1
Error 11/12/2025 08:47:27 [RhinoCode] pip install error | 
ERROR: Invalid requirement: '<3.0': Expected package name at the start of dependency specifier
    <3.0
    ^

@gonzalocasas
Copy link
Copy Markdown
Member

omg.. haha.. have we reported it to them? If so, please add a link to the forum post or similar so that eventually we read this again, and we understand why we did this. Maybe also add a # HACK: thingy so that we will remember to remove it

@chenkasirer
Copy link
Copy Markdown
Member Author

Code looks good to me, but I haven't really understood what's the use-case this solves, in dev mode, I would I assume the components don't really need any # r: and one would have a custom python component running first in the GH file that has the # r: with all the locally relevant stuff, but I am surely missing something!

This makes it easier to create isolated dev environments for plugin development. so for example invoke update-gh-header --dev --venv ct_unreleased results in the following header:

# venv: ct_unreleased
# env: C:\Users\ckasirer\Documents\repos\compas_timber\src
# r: rtree, compas>=2.0, compas_model == 0.9.1

which is all one needs in order to get this component to run, backed by an editable version of the library.

this is part of a workflow that looks like this:

  1. invoke update-gh-header --dev --venv ct_unreleased to set the header
  2. invoke build-cpython-ghuser-components to build the components
  3. invoke yakerize ... ... to package everything up
  4. drag into Rhino and you're done

@gonzalocasas
Copy link
Copy Markdown
Member

Ahh makes sense now! Thanks

@chenkasirer
Copy link
Copy Markdown
Member Author

omg.. haha.. have we reported it to them? If so, please add a link to the forum post or similar so that eventually we read this again, and we understand why we did this. Maybe also add a # HACK: thingy so that we will remember to remove it

opened https://discourse.mcneel.com/t/python-dependencies-with-ordered-comparison-syntax/212304!

@chenkasirer chenkasirer merged commit b9b1572 into main Nov 12, 2025
11 checks passed
@chenkasirer chenkasirer deleted the deps_to_r branch November 12, 2025 10:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants