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

Stacked @card v1.2 : Customizing @card with current.card #894

Merged
merged 53 commits into from
Jan 25, 2022

Conversation

valayDave
Copy link
Collaborator

@valayDave valayDave commented Jan 13, 2022

Stacked on : #893

Summary of Changes

This PR is an attempt to make MetaflowCards customizable. Metaflow already ships with MetaflowCardComponents. These are isolated components for a MetaflowCard. With this PR users will be able to dynamically add components in @step code. This PR introduces the current.card interface to collect MetaflowCardComponents from userland (@step code). This PR also introduces the id argument in the @card decorator and the --id option in the card create command.

Core Changes

  1. Introduce a contruct called current.card to collect components from @step code
  2. Addition of id property to @card and cli. id helps resolve custom components to the right card when using multiple decorators.

Since there can be many cards, this PR introduces the concept of user-editable cards. This seemed a necessary bifurcation because decorators can be dynamically added at runtime. So to distinguish what cards users can editing we provide MetaflowCards with a ALLOW_USER_COMPONENTS attribute.

If ALLOW_USER_COMPONENTS is set to True then the MetaflowCard is eligible to have components added from the @step code. We will call this an editable card.

Once the card the editable, we can collect custom components from the step code in the following way :

@card(type='blank',id='a')
@card(type='default')
@step
def train(self):
    from metaflow.cards import Markdown
    from metaflow import current
    current.card['a'].append(Markdown('# This is present in the blank card with id "a"'))
    current.card.append(Markdown('# This is present in the default card'))
    self.t = dict(
        hi = 1,
        hello = 2
    )
    self.next(self.end)

Consider the above snippet. In the above scenario there are two @card decorators. One card is of type blank and another of type default. The current.card object helps collect "components" from @step code to customize some card. In this case, it is a Markdown component. current.card only accepts objects which are subclasses of MetaflowCardComponent. Since it is the responsibility of current.card to resolve a component to the card mentioned in the decorator, current.card first tries to resolve a default editable card. A default editable card is a card that will have access to the current.card.append/current.card.extend methods. Since there can be many decorators and hence many cards, current.card.append resolves this once the last @card decorators call task_pre_step. In the above case when the user calls append, metaflow will only add Markdown to the default card. current.card['a'].append will add the Markdown to the blank card. Here a is an id that can help resolve the component to the card. Since cards can be of many types, some cards can also not be user-editable (Cards with ALLOW_USER_COMPONENTS=False). Those cards won't be eligible to access the current.card.append

current.card (CardComponentCollector)

The CardComponentCollector is the object responsible for resolving a MetaflowCardComponent to the card provided in the @card decorator.

Since there can be many cards, CardComponentCollector has a _finalize function. The _finalize function is called once the last @card decorator calls task_pre_step. The _finalize function will try to find the default editable card from all the @card decorators on the @step. The default editable card is the card that can access the current.card.append/current.card.extend methods. If there are multiple editable cards with no id then current.card will throw warnings when users call current.card.append. This is done because we cannot resolve which card the component belongs.

The @card decorator also exposes another argument called customize=True. Only one @card decorator over a @step can have customize=True. Since cards can also be added from CLI when running a flow, adding @card(customize=True) will set that particular card from the decorator as default editable. This means that current.card.append will append to the card belonging to @card with customize=True. If there is more than one decorator with customize=True we throw warnings that current.card.append won't append to any card.

One important feature of the current.card object is that it will not fail. Even when users try to access current.card.append with multiple editable cards, we throw warnings but don't fail. current.card will also not fail when a user tries to access a card of a non-existing id via current.card['mycard']. Since current.card['mycard'] gives reference to a list of MetaflowCardComponents, we return a non referenced list when users try to access current.card['my_non_existant_card']

TLDR

  • current.card.append customizes the default editable card.
  • Only one card can be default editable in a step.
  • The card class must have ALLOW_USER_COMPONENTS=True to be considered default editable.
  • Classes with ALLOW_USER_COMPONENTS=False are never default editable.
  • The user can specify an id argument to a card, in which case the card is editable through current.card[id].append.
  • A card with an id can be also defaulted editable if there are no other cards that are eligible to be default editable.
  • If multiple default-editable cards exist but only one card doesn’t have an id, the card without an id is considered to be default editable.
  • If we can’t resolve a single default editable card through the above rules, current.card.append calls show a warning but the call doesn’t fail.
  • A card which is not default editable can be still edited through:
    • current.card['myid']
    • by looking it up by its type, e.g. current.card.get(type=’pytorch’).

Technical Details of Changes

Since the resolution of the default editable card is important for current.card, we need to ensure that we call the _finalize function when the last @card decorator calls task_pre_step. To keep track of this the changes introduce class methods to set the total number of card decorators per @step and increment a global step counter keeping a track of the number of decorators that have called task_pre_step. Once the last @card per @step calls task_pre_step we call the _finalize function.

The total number of card decorators per step is set in the step_init callback and the instantiation/addition of cards to the CardComponentCollector happens in the task_pre_step. Once the user code finishes running we serialize the MetaflowCardComponents added to current.card. CardComponentCollector has a _serialize function which helps
serialize the components of the card to a JSON list or a list of strings. In the serialization process it calls the MetaflowCardComponent.render() function. The MetaflowCardComponent.render() is expected to return a JSON serializable dict or string. This list is json dumped to a tempfile and passed to the card create subprocess.

The create command in the card_cli.py reads the component file as-is and passes the serialized component list when instantiating the card class.

-  Added api for `current.cards` that handles multiple decorators
-
- Introduced `ALLOW_USER_COMPONENTS` attribute to `MetaflowCard` class
- setting `ALLOW_USER_COMPONENTS=True` allow editable cards
- Modified logic of `CardComponentCollector`
- `CardComponentCollector.append` only accessible to default editble card
- tiny refactor at `CardDecorator._is_event_registered("step-init")`
- `customize=True` only allowed for one @card deco per @step
- `customize=True` sets the card to be default editable
- `current.card.append` appends to @card with `customize=True`
- Added `current.cards.get(type='default')` method
- All cards with `id` are accessible via `current.cards['xyz']`
- `current.cards['xyz']` can also return non-editable cards
- Added more comments and polished older comments
- Added better warning messages.
- Shutting off `current.card[]` interface for invalid card ids .
- Setting per step @card count to ensure correct initialization of `current.cards`
- paring decospecs from `--with` doesn't conserve type.
- Typecasting needs to be done in some form because of ambiguity of instantiation from cli and code.
- stateful warnings all user facing interfaces.
- Adds warning once.
- Option to disable it too.
@valayDave valayDave changed the title Mfcards s1 current card Customizing @card with current.card Jan 13, 2022
@valayDave valayDave changed the title Customizing @card with current.card Stacked @card v1.2 : Customizing @card with current.card Jan 13, 2022
Copy link
Contributor

@romain-intel romain-intel left a comment

Choose a reason for hiding this comment

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

Didn't look at everything in detail but hopefully some useful comments.

metaflow/plugins/cards/card_cli.py Outdated Show resolved Hide resolved
metaflow/plugins/cards/card_decorator.py Outdated Show resolved Hide resolved
metaflow/plugins/cards/card_decorator.py Outdated Show resolved Hide resolved
and re.match(CARD_ID_PATTERN, self.attributes["id"]) is None
):
# There should be a warning issued to the user that `id` doesn't match regex pattern
# Since it is doesn't match pattern, we need to ensure that `id` is not accepted by `current`
Copy link
Contributor

Choose a reason for hiding this comment

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

multiple typos in these two lines :)

@@ -63,10 +79,31 @@ def _walk(self, root):
p = os.path.join(path, fname)
yield p, p[prefixlen:]

def _is_event_registered(self, evt_name):
Copy link
Contributor

Choose a reason for hiding this comment

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

simplify: return evt_name in self._called_once

metaflow/plugins/cards/component_serializer.py Outdated Show resolved Hide resolved
metaflow/plugins/cards/component_serializer.py Outdated Show resolved Hide resolved
metaflow/plugins/cards/component_serializer.py Outdated Show resolved Hide resolved
metaflow/plugins/cards/component_serializer.py Outdated Show resolved Hide resolved
valayDave and others added 27 commits January 14, 2022 11:51
- Renaming variables.
- uuid to have a uuidv4
- simple typo fixes
- step init sets card counts only once.
- changed default for `suppress_warnings`
- Changed `"%s/%s"` pattern to os path join
…895)

* Added the `--id` option to `card view`/`card get`
- modified datastore and fixed the exception class.
- Allowing some ambiguity in pathspec argument for `get/view` command.

* Added `id` argument to `get_cards`

* Bringing `card list` cli functionality from test-suite branch
- added functionality to list cards about a task and even list it as JSON.

* changed hash checking logic.
- instead of equating we check `startswith`

* Added list many feature for `card list`
- listing all cards from the latest run when card list is called with no argument

* Added `card list` print formatting changes
- `--as-json` works for many cards and single cards


* Stacked @card v1.2 : New `MetaflowCardComponent`s  (#896)

* user-facing `MetaflowCardComponent`s
- import via `from metaflow.cards import Artifact,..`

* 7 Major Changes:
- Removed `Section` component from `metaflow.cards`
- Making user-facing component inherent to `UserComponent` which in turn inherits `MetaflowCardComponent`
- Wrap all `UserComponents` inside a section after rendering everything per card
- created a `render_safely` decorator to ensure fail-safe render of `UserComponent`s
- removed code from component serializer which used internal components
- Refactored some components that return render
- Added docstrings to all components.

* JS + CSS + Cards UI Build

* Stacked @card v1.2 : Graph Related Changes to card cli (#911)

* accomodating changes from #833
- Minor tweaks to `graph.py`

* Stacked @card v1.2 : Namespace Packages with `@card` (#897)

* setup import of cards from `metaflow_extensions` using `metaflow.extension_support`
- Added import modules in `card_modules`
- Added hook in card decorators to add custom packages

* Added some debug statements for external imports.

* Stacked @card v1.2 : Test cases for Multiple `@card`s (#898)

* Multiple Cards Test Suite Mods (#27)

- Added `list_cards` to `CliCheck` and `MetadataCheck`
- Bringing #875 into the code
- Added a card that prints taskspec with random number

* Added test case for multiple cards

* Added tests card, summary :
- `current.cards['myid']` should be accessible when cards have an `id` argument in decorator
- `current.cards.append` should not work when there are no single default editable card.
- if a card has `ALLOW_USER_COMPONENTS=False` then it can still be edited via accessing it with `id` property.
- adding arbitrary information to `current.cards.append` should not break user code.
- Only cards with `ALLOW_USER_COMPONENTS=True` are considered default editable.
- If a single @card decorator is present with `id` then it `current.cards.append` should still work
- Access of `current.cards` with non existant id should not fail.
- `current.cards.append` should be accessible to the card with `customize=True`.
-

* fixed `DefaultEditableCardTest` test case
- Fixed comment
- Fixed the `customize=True` test case.

* ensure `test_pathspec_card` has no duplicates
- ensure entropy of rendered information is high enough to not overwrite a file.

* test case fix : `current.cards` to `current.card`

* Added Test case to support import of cards.
- Test case validates that broken card modules don't break metaflow
- test case validates that we are able to import cards from metaflow_extensions
- Test case validate that cards can be editable if they are importable.

* Added Env var to tests to avoid warnings added to cards.

* Added Test for card resume.

* Stacked @card v1.2: Card Dev Docs (#899)

Co-authored-by: Romain Cledat <rcledat@netflix.com>

Co-authored-by: Brendan Gibson <brendan@outerbounds.co>
Co-authored-by: Brendan Gibson <93726128+obgibson@users.noreply.github.com>
Co-authored-by: adam <203779+seethroughdev@users.noreply.github.com>
Co-authored-by: Romain Cledat <rcledat@netflix.com>

Co-authored-by: Brendan Gibson <brendan@outerbounds.co>
Co-authored-by: Brendan Gibson <93726128+obgibson@users.noreply.github.com>
Co-authored-by: adam <203779+seethroughdev@users.noreply.github.com>
Co-authored-by: Romain Cledat <rcledat@netflix.com>
@valayDave valayDave merged commit 0148bb6 into mfcards-s0-multi-deco Jan 25, 2022
@valayDave valayDave deleted the mfcards-s1-current-card branch January 25, 2022 04:43
oavdeev pushed a commit that referenced this pull request Jan 25, 2022
* multiple decorator support

* Fixing comments

* comment fix.

* commet fix

* allow multiple decorators of same type from cli

* Stacked @card v1.2 : Customizing `@card` with `current.card` (#894)

* allow passing userland `MetaflowCardComponents`
-  Added api for `current.cards` that handles multiple decorators

* `current.cards` with `id` support.
- Introduced `ALLOW_USER_COMPONENTS` attribute to `MetaflowCard` class
- setting `ALLOW_USER_COMPONENTS=True` allow editable cards
- Modified logic of `CardComponentCollector`
- `CardComponentCollector.append` only accessible to default editble card

* Added `customize=True` in `@card`
- `customize=True` only allowed for one @card deco per @step
- `customize=True` sets the card to be default editable
- `current.card.append` appends to @card with `customize=True`


* Stacked @card v1.2: Read cli changes for Supporting Multiple`@card`s (#895)

* Added the `--id` option to `card view`/`card get`
- modified datastore and fixed the exception class.
- Allowing some ambiguity in pathspec argument for `get/view` command.

* Added `id` argument to `get_cards`

* Bringing `card list` cli functionality from test-suite branch
- added functionality to list cards about a task and even list it as JSON.

* changed hash checking logic.
- instead of equating we check `startswith`

* Added list many feature for `card list`
- listing all cards from the latest run when card list is called with no argument

* Added `card list` print formatting changes
- `--as-json` works for many cards and single cards


* Stacked @card v1.2 : New `MetaflowCardComponent`s  (#896)

* user-facing `MetaflowCardComponent`s
- import via `from metaflow.cards import Artifact,..`

* 7 Major Changes:
- Removed `Section` component from `metaflow.cards`
- Making user-facing component inherent to `UserComponent` which in turn inherits `MetaflowCardComponent`
- Wrap all `UserComponents` inside a section after rendering everything per card
- created a `render_safely` decorator to ensure fail-safe render of `UserComponent`s
- removed code from component serializer which used internal components
- Refactored some components that return render
- Added docstrings to all components.

* JS + CSS + Cards UI Build

* Stacked @card v1.2 : Graph Related Changes to card cli (#911)

* accomodating changes from #833
- Minor tweaks to `graph.py`

* Stacked @card v1.2 : Namespace Packages with `@card` (#897)

* setup import of cards from `metaflow_extensions` using `metaflow.extension_support`
- Added import modules in `card_modules`
- Added hook in card decorators to add custom packages

* Added some debug statements for external imports.

* Stacked @card v1.2 : Test cases for Multiple `@card`s (#898)

* Multiple Cards Test Suite Mods (#27)

- Added `list_cards` to `CliCheck` and `MetadataCheck`
- Bringing #875 into the code
- Added a card that prints taskspec with random number

* Added test case for multiple cards

* Added tests card, summary :
- `current.cards['myid']` should be accessible when cards have an `id` argument in decorator
- `current.cards.append` should not work when there are no single default editable card.
- if a card has `ALLOW_USER_COMPONENTS=False` then it can still be edited via accessing it with `id` property.
- adding arbitrary information to `current.cards.append` should not break user code.
- Only cards with `ALLOW_USER_COMPONENTS=True` are considered default editable.
- If a single @card decorator is present with `id` then it `current.cards.append` should still work
- Access of `current.cards` with non existant id should not fail.
- `current.cards.append` should be accessible to the card with `customize=True`.
-

* fixed `DefaultEditableCardTest` test case
- Fixed comment
- Fixed the `customize=True` test case.

* ensure `test_pathspec_card` has no duplicates
- ensure entropy of rendered information is high enough to not overwrite a file.

* test case fix : `current.cards` to `current.card`

* Added Test case to support import of cards.
- Test case validates that broken card modules don't break metaflow
- test case validates that we are able to import cards from metaflow_extensions
- Test case validate that cards can be editable if they are importable.

* Added Env var to tests to avoid warnings added to cards.

* Added Test for card resume.

* Stacked @card v1.2: Card Dev Docs (#899)

Co-authored-by: Brendan Gibson <brendan@outerbounds.co>
Co-authored-by: Brendan Gibson <93726128+obgibson@users.noreply.github.com>
Co-authored-by: adam <203779+seethroughdev@users.noreply.github.com>
Co-authored-by: Romain Cledat <rcledat@netflix.com>

* Nit fix in error.

* Fixing bug in decorator

* whitespace.

* Changing import scheme of warning variable.

* fixing warning suppression env var setting

Co-authored-by: Brendan Gibson <brendan@outerbounds.co>
Co-authored-by: Brendan Gibson <93726128+obgibson@users.noreply.github.com>
Co-authored-by: adam <203779+seethroughdev@users.noreply.github.com>
Co-authored-by: Romain Cledat <rcledat@netflix.com>
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.

2 participants