Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!(services): add a project service #646

Merged
merged 42 commits into from
Feb 27, 2025

Conversation

lengau
Copy link
Contributor

@lengau lengau commented Feb 13, 2025

  • Have you followed the guidelines for contributing?
  • Have you signed the CLA?
  • Have you successfully run make lint && make test?
  • Have you added an entry to the changelog (docs/reference/changelog.rst)?

This creates a ProjectService for handling projects. There are a few tests that this causes issues with because it messes with build plans, so those are being skipped until I implement the BuildPlanService.

This is pretty huge, so I'm providing a review methodology. I'd recommend marking each file as viewed as you complete it, even if you have comments, because there's a "draw the rest of the owl" step at the end.

  1. Start with the Project service loading docs (rendered), which should provide you a good overview of how the project service works.
  2. Review the ProjectService itself. This is the actual core of the new logic.
  3. Review the project service's unit tests
  4. Review its integration tests, including the example input/output files.
  5. Review the changes to the Application, which remove project rendering. Here's a good spot to look for anything I may have overlooked when moving the logic.
  6. Review the changes to the other services. I'll put in some implementation notes that are only really relevant when considering the changes.
  7. Review the new fixtures for this.
  8. Note that we no longer have a ProjectService abstract class
  9. Likewise, the Service factory now no longer checks for a project before creating services
  10. Review the rest of the services
  11. Review any files you haven't already reviewed.

CRAFT-4158

@lengau lengau force-pushed the work/project-service branch 6 times, most recently from 3736290 to 5c62503 Compare February 20, 2025 21:19
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@cmatsuoka this is the bulk of the actual change to the business logic. It (and its related test) is already reviewable.

Copy link
Contributor

Choose a reason for hiding this comment

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

Looking good so far.

This makes the ServiceFactory no longer a dataclass. It's only a
dataclass for historical reasons around the old way of registering
services, but this is no longer necessary.
We don't always have that call at the beginning of the interactions.
Removes the ability to use secrets. This can be added to applications if
necessary using the prototype secrets service.
@lengau lengau force-pushed the work/project-service branch from 40d2762 to 9502d62 Compare February 21, 2025 18:05
@lengau lengau changed the title feat!(partial): project service feat!(services): add a project service Feb 21, 2025
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This might be a bit much infrastructure for now, but I'd rather have it than need to make forwards later.

self._project_dir = project_dir
self._project_model = None

def resolve_project_file_path(self) -> pathlib.Path:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Please review these docstrings both as-is and in the context of the autodocs.

@lengau lengau marked this pull request as ready for review February 21, 2025 20:53


class ProjectService(base.AppService):
"""A service for handling access to the project."""
Copy link
Collaborator

Choose a reason for hiding this comment

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

I feel this to be scant but as I tried to write something up, I could not think of anything meaningful

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah that's why I didn't include more. Every time I tried to add something it seemed redundant to the documentation page.

Maybe a reference to the documentation page is what I should add?

Comment on lines 113 to 114
The application must use the provided parameters rather than service state
when applying ``build_on``, ``build_for`` or ``platform`` information.
Copy link
Collaborator

Choose a reason for hiding this comment

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

this needs rewording, or expanding

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it might need removing (and replacing). My first implementation of the ProjectService had those three as instance attributes as well, but this ended up complicating the implementation. I'll rewrite to include some real-world use cases.

---------------

The project service is responsible for loading, validating and rendering the project
file to a ``Project`` model. While the service can render a project as though it is
Copy link
Collaborator

Choose a reason for hiding this comment

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

can you reference Project even if it is the base model?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've changed it, though the rendered version won't change because we don't include the class's reference in docs. I've created #662 for that.

Comment on lines 247 to 248
arch=build_on,
target_arch=str(build_for),
Copy link
Contributor

Choose a reason for hiding this comment

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

Why one needs str() and another one does not ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

build_for used to be a DebianArchitecture or the string all. The type change is no longer needed.

Comment on lines 181 to 184
raise RuntimeError(
"Somehow the host architecture is all Debian architectures. "
"My universe is shattered; I give up."
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Even if this is very unlikely, we should have a dedicated error for that, that Host architecture couldn't be resolved, so we can't build for "all".

Copy link
Contributor 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 we need a dedicated error, because this is an error in the application. The application would need to provide a version of craft_platforms that only has a single value in DebianArchitecture that matches. I'll rewrite the message to be less prototype-y though.

@@ -257,6 +256,8 @@ def get(self, service: Literal["package"]) -> services.PackageService: ...
@overload
def get(self, service: Literal["lifecycle"]) -> services.LifecycleService: ...
@overload
def get(self, service: Literal["project"]) -> ProjectService: ...
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not services.ProjectService ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Eventually I want to remove the individual services from the services module directly so we don't even have to load built-in services if we don't use them. ProjectService only gets imported here in type-checking mode or when actually requesting it.

# Workaround until we implement CRAFT-4159
app._bootstrap_services() # We need to access the project service.
app.services.get("project").render_once()
if date.today() < date(2025, 3, 1):
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it a good idea to put here 2 weeks deadline 😄 ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We'll have to extend it if I don't finish the build plan service by the end of the pulse, but I'd rather extend it than have it accidentally go into main.

@@ -591,6 +592,7 @@ def test_lifecycle_package_repositories(
platform=None,
build_plan=fake_build_plan,
)
service.setup()
Copy link
Contributor

Choose a reason for hiding this comment

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

Why we need to setup it now?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because setup gets the project.

@@ -251,8 +251,7 @@ def test_get_unregistered_service(factory):


def test_get_project_service_error(factory):
factory.project = None
with pytest.raises(ValueError, match="LifecycleService requires a project"):
with pytest.raises(RuntimeError, match="Project not rendered yet."):
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it a good error ? It is suggesting that it will render at some point

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure what better wording would be appropriate. Maybe "attempted to get the project before it was rendered"?

Copy link
Contributor

@dariuszd21 dariuszd21 left a comment

Choose a reason for hiding this comment

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

I like the idea and how it looks like.
The comments I left are mostly to clarify certain aspects and to see if I understand it correctly.
From what I can see, it shouldn't affect how the commands that conditionally require project works and for the others change is rather transparent (even if non backward-compatible)

@tigarmo tigarmo self-requested a review February 25, 2025 15:10
@lengau lengau requested a review from sergiusens February 25, 2025 18:30
Copy link
Contributor

@tigarmo tigarmo left a comment

Choose a reason for hiding this comment

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

This is great :) the review script is specially appreciated! I think this is overall "there" but I had some questions before approving the PR
thanks!

@@ -300,6 +301,17 @@ def cache_dir(self) -> pathlib.Path:
f"Unable to create/access cache directory: {err.strerror}"
) from err

def _bootstrap_services(self) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd like to suggest _configure_early_services(), so that the difference between it and _configure_services is more evident

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea!

@@ -332,6 +344,9 @@ def _resolve_project_path(self, project_dir: pathlib.Path | None) -> pathlib.Pat
directory. Applications may wish to override this if the project file could be
in multiple places within the project directory.
"""
warnings.warn(
DeprecationWarning("NEEDS REMOVING BEFORE MERGE TO MAIN"), stacklevel=1
Copy link
Contributor

Choose a reason for hiding this comment

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

is this being removed on a later task?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for catching this! At first I thought it would need to stay until a future task, but it looks like I have indeed removed all usages.

project_service = self.services.get("project")
# This branch always runs, except during testing.
if not project_service.is_rendered:
project_service.render_once()
Copy link
Contributor

Choose a reason for hiding this comment

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

what about the platform and build_for parameters? How do they end up in the service in this specific code path?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch, thanks!

)

@final
def get_platforms(self) -> dict[str, craft_platforms.PlatformDict]:
Copy link
Contributor

Choose a reason for hiding this comment

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

how come this method is public? Who, do you figure, will need to use this? Other than tests I mean

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The BuildPlanService uses it to generate the build plan. This is also the primary reason for having _app_render_legacy_platforms() - the BuildPlanService expects to get a platforms entry that craft-platforms can use to generate the exhaustive build plan.

@@ -337,21 +337,6 @@ def _configure_services(self, provider_name: str | None) -> None:
session_policy=self._fetch_service_policy,
)

def _resolve_project_path(self, project_dir: pathlib.Path | None) -> pathlib.Path:
Copy link
Contributor

Choose a reason for hiding this comment

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

just to double-check: is snapcraft expected to just drop the override on their version? Or are there more changes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Snapcraft will need to move that override to the ProjectService's `resolve_project_file_path()'. Should work roughly the same though (just has to use the class attribute for project dir rather than a method arg)

Copy link
Contributor

Choose a reason for hiding this comment

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

My concern is that Snapcraft calls that one extremely early, before even _configure_early_services() has been called

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this is worth sweating too much just now; since this is going to a feature branch we'll have time to assess the changes in Snapcraft before setting anything in stone.

Copy link
Contributor

@tigarmo tigarmo left a comment

Choose a reason for hiding this comment

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

🚀

@tigarmo
Copy link
Contributor

tigarmo commented Feb 26, 2025

Regarding the changelog: Since there are more changes incoming, do you want to draft the changes for the project service now, or do a unified changelog entry once the feature-branch is ready to be merged to main?

@lengau
Copy link
Contributor Author

lengau commented Feb 26, 2025

@tigarmo to your point about the changelog: I'll be requesting a full documentation review before merge to main, so I'm not requesting it right now on the feature branch. However, I have added the relevant changes so I don't forget them. Thanks!

@lengau lengau merged commit 83ca560 into feature/hybrid-commands Feb 27, 2025
15 checks passed
@lengau lengau deleted the work/project-service branch February 27, 2025 15:13
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.

5 participants