Skip to content

File associations#2284

Merged
freakboy3742 merged 38 commits intobeeware:mainfrom
davidfokkema:file-associations
Jun 13, 2025
Merged

File associations#2284
freakboy3742 merged 38 commits intobeeware:mainfrom
davidfokkema:file-associations

Conversation

@davidfokkema
Copy link
Contributor

@davidfokkema davidfokkema commented May 13, 2025

This PR adds file associations to macOS apps. The support in briefcase.toml is already there and the feature is documented. It wasn't yet generalised for non-package file types.

Refs #1706 (the issue)
Refs beeware/briefcase-macOS-app-template#72 (the PR for the macOS app template)
Refs beeware/briefcase-macOS-Xcode-template#82 (the PR for the macOS Xcode template)

This PR completes the work in the briefcase CLI tool.

I tested with https://github.com/davidfokkema/helloworld. This repository may be removed when the PR is merged.

PR Checklist:

  • All new features have been tested
  • All new features have been documented
  • I have read the CONTRIBUTING.md file
  • I will abide by the code of conduct

Copy link
Member

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

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

The mechanics of this look good; a couple of minor comments, and obviously the testing issues still need to be addressed.

@davidfokkema
Copy link
Contributor Author

I have two code coverage issues of which I'm not sure how to proceed. The aforementioned exception if the plist file is not in the correct location works fine in manual testing, but is not a part of the tests since the location is hardcoded in the file. So no 100% coverage. Should I change the path to a global variable, alter that in testing to check for the exception? Or should I skip the coverage for that branch?

Second issue is the if-statement in document validation where it checks the platform. If it is not macOS, it should not do the mime type to UTI conversion. I never test on macOS that it is not macOS, so coverage.py complains.

@freakboy3742
Copy link
Member

I have two code coverage issues of which I'm not sure how to proceed. The aforementioned exception if the plist file is not in the correct location works fine in manual testing, but is not a part of the tests since the location is hardcoded in the file. So no 100% coverage. Should I change the path to a global variable, alter that in testing to check for the exception? Or should I skip the coverage for that branch?

There's a couple of possible approaches I can see - but your idea of putting the name of the file as a constant and then mocking/monkeypatching that constant for the purposes of the "file doesn't exist" test sounds good to me.

Second issue is the if-statement in document validation where it checks the platform. If it is not macOS, it should not do the mime type to UTI conversion. I never test on macOS that it is not macOS, so coverage.py complains.

What we really need here is a conditional no-branch... but I don't think our conditional coverage handling can do that. The best fix I can see there is to put in a no-op else: pass, and mark that else as no-cover-if-is-macos.

@davidfokkema
Copy link
Contributor Author

There's currently one failed unit test because of a timeout which I don't really understand since it seems that the failing test mocks a download, so... should never time out? Not sure. Restarting the GitHub action will probably solve this for now.

@freakboy3742
Copy link
Member

There's currently one failed unit test because of a timeout which I don't really understand since it seems that the failing test mocks a download, so... should never time out? Not sure. Restarting the GitHub action will probably solve this for now.

Hrm... I don't think it's doing a download, but it may be probing a live URL, which would make it sensitive to a temporary network outage. I haven't seen that failure before, though - I've kicked off the test again, and it looks like it's passed this time.

Specifying multiple values in a LSItemContentTypes key is only valid
when multiple document types are manually grouped together in the
Info.plist file. For Briefcase apps, document types are always
separately declared in the configuration file, so only a single value
must be provided. Because the key name is plural and is an array in the
Info.plist file, a list *is* accepted, but must have only one element.
Since the code to pass coverage was ugly and should be removed in any case, I'm now passing pre-commit. I'm really not sure why coverage.py keeps complaining the branch is never reached, when it clearly _is_.
@davidfokkema
Copy link
Contributor Author

A new issue has arisen: coverage. Coverage.py keeps complaining one branch of an if-statement is never reached in the tests, but I know for a fact that it is reached. If I include an else: 1 / 0 then the code crashes, if I include an else: foo = 1 then the code does not crash and coverage.py is happy, but apart from the ugly hack the pre-commit step fails because foo is never used. I could fix that with another line foo += 1 or something like that but that is even uglier. I removed it, and maybe someone else can figure out why coverage.py complains. I believe passing a string to LSItemContentTypes takes the 'missing' branch, and that is tested in the test_document_type_macOS_config_with_list_of_single_content_type test in tests/config/test_document_type_config.py.

@mhsmith
Copy link
Member

mhsmith commented May 21, 2025

The coverage error is src/briefcase/config.py 168->exit. This means that the if statement on line 168 has been executed, but it always evaluates to true. So either the if is redundant and can be removed, or you need to add a test for the case where the if evaluates to false.

@davidfokkema
Copy link
Contributor Author

The coverage error is src/briefcase/config.py 168->exit. This means that the if statement on line 168 has been executed, but it always evaluates to true. So either the if is redundant and can be removed, or you need to add a test for the case where the if evaluates to false.

I don't have much experience with coverage.py, so thanks for taking a look! The if-statement checks whether the supplied content type is a list, and the 'else' means 'no list'. I believe I test that here:

valid_document.setdefault("macOS", {})["LSItemContentTypes"] = "com.adobe.pdf"
. If I add an else: 1 / 0 statement that test crashes. So either I'm doing something wrong, I'm missing something obvious, or coverage.py is a bit confused.

@mhsmith
Copy link
Member

mhsmith commented May 21, 2025

Are you sure you're adding the else at the correct indentation level? There are several nested ifs here.

...but I don't like the fix.
Comment on lines 181 to 182
else:
True is True
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These two lines seem to fix the coverage check, but obviously they don't do anything.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mhsmith, but this fails the ruff pre-commit check with https://docs.astral.sh/ruff/rules/useless-comparison/. Of course. I don't know what the best course of action is.

Copy link
Member

Choose a reason for hiding this comment

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

We had a similar mysterious coverage failure in #2308, but I don't think we ever worked out the cause.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, I see the commit, thanks. I've now pushed a no-op which hopefully satisfies both the coverage checker and the pre-commit check and will leave that in for now. If it's a bug in coverage.py, this will hopefully be fixed in the future and this can go out. Not sure if I have enough time and inclination to create an MRE from this to file as an issue on coverage.py.

Copy link
Member

@freakboy3742 freakboy3742 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 getting pretty close; a couple of minor tweaks and suggestions inline.

def test_mime_type_to_uti_with_nonexisting_coretypes_file(monkeypatch):
"""Test that mime_type_to_UTI returns None if the coretypes file doesn't exist."""
monkeypatch.setattr(utils, "CORETYPES_PATH", "/does/not/exist")
assert utils.mime_type_to_UTI("application/pdf") is None
Copy link
Member

Choose a reason for hiding this comment

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

For consistency, this should either (a) be test at the validate_document_type_config() level (i.e., a reproduction of the actual application/pdf case, but with the "couldn't find UTI data" response, or a separate test in the macOS module that does a couple of superficial direct checks of mime_type_to_UTI().

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 I follow. Could you elaborate, please?

Copy link
Member

Choose a reason for hiding this comment

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

All the other tests in this file are based on invoking validate_document_type_config(); but this test is invoking utils.mime_type_to_UTI. The test suite is broadly organised by test function; a test of mime_type_to_UTI should either be in a test file for that method; or the entry point for for this specific test should be modified to use validate_document_type_config() (i.e., testing the same thing, but doing so by setting up test conditions that will result in performing a mime type lookup).

To that end - this test file should also be called test_validate_document_type_config.

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 reshuffled the tests. Renamed test_document_type_config.py to test_validate_document_type_config.py and split out test_is_uti_core_type.py and test_mime_type_to_uti.py, and the first test file no longer depends on the briefcase.platforms.macOS.utils module.

@davidfokkema
Copy link
Contributor Author

(At least) one failing test needs a rerun:

Unable to download RCEdit; is your computer offline?

@davidfokkema
Copy link
Contributor Author

Added some documentation to describe the document types, especially the new macOS-specific support. Probably a work in progress, but it is a first draft.

@freakboy3742
Copy link
Member

FYI - I wanted to let you know that I'm not ignoring this PR; I'm currently buried in post-PyCon US backlog. I'm hoping to get to this in a day or two.

@davidfokkema
Copy link
Contributor Author

Sure, no worries!

Copy link
Member

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

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

First off - apologies for the delay in reviewing this. "A couple of days" turned out to be overly optimistic.

But - this is awesome stuff. I've done an update pass on the documentation, and tweaked some tests (mostly as penance for being so slow on the review); but I think this is good to go. Thanks for all your work on this - Briefcase's doctype handling is a lot more robust as a result of your efforts.

Comment on lines 675 to 676
``application/x-myapp-data``. This is not a valid MIME type, and should not be
used for production applications. It is only used for testing purposes, to allow
Copy link
Member

Choose a reason for hiding this comment

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

AFAIK, this is incorrect. The application/x- prefix is defined by RFC 2046 for "private and experimental" MIME types - it exists specifically for expansion by end users without needing to go through formal IANA registration - so it is valid.

Background
~~~~~~~~~~

First, macOS document types are defined in the ``Info.plist`` file, and the
Copy link
Member

Choose a reason for hiding this comment

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

These docs are great from the perspective of macOS internals, but they're not as practical for a Briefcase user. Reading these docs, my immediate question was "ok... but what do I have to do?".

And - for most users, the answer is "nothing". I've pushed an update to these docs that changes the focus to more of a "how does Briefcase use these settings", rather than trying to explain Apple's Info.plist scheme.

elif isinstance(content_types, str):
# If the content type is a string, convert it to a list
macOS["LSItemContentTypes"] = [content_types]
else:
Copy link
Member

Choose a reason for hiding this comment

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

These no-op branches can be cleaned up if the content type handling is performed before the UTI handling - I'll push an update to do this.

"extension": "doc",
"description": "A document",
"url": "https://testurl.com",
"mime_type": "text/x.my-doc-type",
Copy link
Member

Choose a reason for hiding this comment

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

This isn't a valid doctype - we should be using application/x-my-doc-type.



@pytest.mark.skipif(sys.platform != "darwin", reason="Test runs only on macOS")
def test_is_uti_core_type():
Copy link
Member

Choose a reason for hiding this comment

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

This test can be parameterised.



@pytest.mark.skipif(sys.platform != "darwin", reason="Test runs only on macOS")
def test_mime_type_to_uti():
Copy link
Member

Choose a reason for hiding this comment

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

This test can be parameterized.

@freakboy3742 freakboy3742 merged commit e80edff into beeware:main Jun 13, 2025
57 checks passed
@davidfokkema
Copy link
Contributor Author

No need to apologise, I first started working on this back in March, 2024, but did not have time to finish this for many, many months. Thank you for your encouragement, guidance, and careful reviews throughout the process! I've looked through your latest changes; I like them. Thanks for merging!

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