Skip to content

Combine extract-meta and component generation in a cli. #451

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

Merged
merged 25 commits into from
Nov 29, 2018
Merged

Conversation

T4rk1n
Copy link
Contributor

@T4rk1n T4rk1n commented Nov 6, 2018

  • Add extract-meta.js
  • Combine extract-meta.js call and component generation.
  • Wrap as a cli under the command dash-generate-components

Usage:
$ dash-generate-components src/lib/components my_components_namespace

cc @plotly/dash

Closes plotly/dash-component-boilerplate#17

@nicolaskruchten
Copy link
Contributor

This seems like a good step forward. A few questions:

  1. Is this backwards-compatible with existing components written using the boilerplate?
  2. Can we see the matching PR in the boilerplate repo that calls this new script?
  3. Should we wait until WIP Initial version of updated code for R transpiler #449 is merged and refactor the two together, perhaps with an --experimental-r-components flag?

@T4rk1n
Copy link
Contributor Author

T4rk1n commented Nov 7, 2018

Is this backwards-compatible with existing components written using the boilerplate?

Yes, the old code was not modified.

Can we see the matching PR in the boilerplate repo that calls this new script?

Coming up soon.

Should we wait until #449 is merged and refactor the two together, perhaps with an --experimental-r-components flag?

Just need to add generate_class_file_r call in the loop next to generate_class_file. Should be easy to refactor, can add the argument with argparse.



# pylint: disable=too-many-locals
def generate_components(component_src, output_dir):
Copy link
Contributor

Choose a reason for hiding this comment

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

I would rename output_dir to project_shortname and not treat it as a path.

cmd = shlex.split('node {} {}'.format(extract_path, component_src),
posix=not is_windows)

namespace = os.path.basename(output_dir)
Copy link
Contributor

Choose a reason for hiding this comment

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

I would just remove namespace as a variable here.

)
sys.exit(1)
# pylint: disable=unbalanced-tuple-unpacking
src, out = sys.argv[1:]
Copy link
Contributor

Choose a reason for hiding this comment

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

again, I would rename this project_shortname instead of out... because the R code won't output there.

component_data['description'],
namespace
)
print('Generated {}/{}.py'.format(namespace, name))
Copy link
Contributor

Choose a reason for hiding this comment

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

for the Python piece, if it turns out that we do need package.json in the Python module directory next to __init__.py then I would do the copying here

Copy link
Contributor

Choose a reason for hiding this comment

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

@nicolaskruchten
Copy link
Contributor

💃 once the tests pass and the internal logic of the program are little bit more language-agnostic :)

component_data['description'],
project_shortname
)
print('Generated {}/{}.py'.format(project_shortname, name))
Copy link
Contributor

Choose a reason for hiding this comment

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

can we move this line into the generate_class_file function? It's Python-specific and there will likely be an R-specific version as well and i don't think it's good to duplicate the path-as-function-of-project_shortname logic here as well :)

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 it's good to duplicate the path-as-function-of-project_shortname logic here as well

I don't understand what you mean by that, just print the component name ?

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 saying that in the case of Python, the file path happens to be project_shortname/name`` but for R it'll be camelCase(project_shortname)/name.R` and I don't want to duplicate that logic here...

We could just print Generated {lang} version of {component_name} instead and bypass the language-specific path details :)

components = []

for component_path, component_data in metadata.items():
name = component_path.split('/')[-1].split('.')[0]
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe name this component_name just for extra clarity/readability

with open(os.path.join(project_shortname, 'metadata.json'), 'w') as f:
json.dump(metadata, f)

with open(os.path.join(project_shortname, '_imports_.py'), 'w') as f:
Copy link
Contributor

Choose a reason for hiding this comment

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

can we move this Python-specific stuff into a Python-specific place? doesn't have to be in this PR but just in general :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's actually duplicate code so let's refactor that for the old method too.


from dash.development.component_loader import generate_imports
Copy link
Contributor

Choose a reason for hiding this comment

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

should this also be a relative import? :)


components = []

for component_path, component_data in metadata.items():
Copy link
Contributor

Choose a reason for hiding this comment

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

as a general comment, this seems to be repeated code with

name = componentPath.split('/').pop().split('.')[0]
... any way to DRY it up?

@T4rk1n
Copy link
Contributor Author

T4rk1n commented Nov 14, 2018

@nicolaskruchten Tests are in plotly/dash-component-boilerplate#40

@rmarren1 Please review

Copy link
Contributor

@rmarren1 rmarren1 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! I think we should fully commit to one method of generating class files.

With this change it looks like you can either

  1. Run the extract-meta.js command manually to get metadata.json
  2. Run dash.development.component_loader.generate_classes pointing to the new metadata.json
    OR
  3. Run dash-generate-components to do all that in one function.

So we can either

  1. In generate_components, we can write metadata.json first, then use a call to dash.development.component_loader.generate_classes from within generate_components to generate the components.
  2. Just delete the dash.development.component_loader.generate_classes function and rely on the CLI.

I think 2 might cause some minor headaches since we would need to update the toolchains in dash-*-components to use the CLI rather than dash.development.component_loader.generate_classes, but also dash.development.component_loader.generate_classes isn't really doing much. I'm pro deleting un-needed code, so I would go with #2 unless there is something I am not thinking of.

Also, I think base_component is getting a bit busy, I would be in favor of moving all the I/O functions to component_generator.py, e.g. generate_class_file, generate_imports, generate_classes_files.

Other than the method to get the path to extract-meta.js is the only critical change here.

@@ -4,6 +4,8 @@
import inspect
import abc
import sys
import textwrap
Copy link
Contributor

Choose a reason for hiding this comment

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

🐱

@@ -466,6 +468,8 @@ def generate_class_file(typename, props, description, namespace):
f.write(import_string)
f.write(class_string)

print('Generated {}'.format(file_name))
Copy link
Contributor

Choose a reason for hiding this comment

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

🐱

).lstrip())


def generate_classes_files(project_shortname, metadata, *component_generators):
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this be generate_class_files to match generate_class and generate_class_file method names?

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, can it be moved up to be next to those methods? Just to keep them together spatially in the code

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, perhaps we can just move these into components_generator.py

package_info_filename='package.json'):
is_windows = sys.platform == 'win32'

extract_path = os.path.abspath(os.path.join(
Copy link
Contributor

Choose a reason for hiding this comment

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

This pattern for accessing the included files has given me problems in the past, from what I researched to fix it we should use https://stackoverflow.com/a/20885799

))

os.environ['NODE_PATH'] = 'node_modules'
cmd = shlex.split('node {} {}'.format(extract_path, components_source),
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 different from ['node', extract_path, component_source]? Never used shlex not sure what this does.

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 use shlex to make sure it works across platforms, had problems on windows passing arguments without the posix=False argument.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds good 👍

# Add an import statement for this component
with open(imports_path, 'a') as f:
f.write('from .{0:s} import {0:s}\n'.format(name))
components = generate_classes_files(namespace, data, generate_class_file)
Copy link
Contributor

Choose a reason for hiding this comment

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

Four lines up is not relevant anymore, write append not used.

@nicolaskruchten
Copy link
Contributor

@rmarren1 I think we need to keep supporting both methods for backwards-compatibility, otherwise older components may break, no?

@T4rk1n
Copy link
Contributor Author

T4rk1n commented Nov 26, 2018

In generate_components, we can write metadata.json first, then use a call to dash.development.component_loader.generate_classes from within generate_components to generate the components.

Writing just to read back seems like a bad pattern, I initially did not write the metadata.json and just generate the components but figured it should still be there for backward compatibility.

Just delete the dash.development.component_loader.generate_classes function and rely on the CLI.

I think they should stay there for backward compatibility. Someone working on a component library should be able to update dash without breaking their build system.

@T4rk1n
Copy link
Contributor Author

T4rk1n commented Nov 26, 2018

@rmarren1

I moved all python class generation code to _py_components_generation, only kept the component, meta and registry in base_component. component_generator should be only code needed for the cli generation. So we'll add the code for R generation in _r_components_generation and use it in component_generator cli under a experimental flag at first. Maybe I should change the name of the cli command to dash-component-generator instead of dash-generate-components to match the name of the module ?

@rmarren1
Copy link
Contributor

rmarren1 commented Nov 29, 2018

💃
I like this organizational change, should help to keep our python / r code separate and keep the base_component file slim.

Deleting the non-cli method of generating component class files would only break build systems for component authors and shouldn't affect things for end users (as long as we keep the load_components method), and this should require a one line change to build:py. I didn't think this was a 'breaking change' since it didn't affect end users, but I agree developers should be able to upgrade without having their build systems break and now am in favor of keeping it. We can just make a note of this and remove it in a future breaking release.

@nicolaskruchten
Copy link
Contributor

@rmarren1 FWIW here's the terminology I use when thinking about our stakeholders:

  • Component Developers: makers of components, users of boilerplate and component-generation.
  • App Developers: makers of Dash apps, installers of components, users of Dash API
  • End Users: users of Dash apps

In general our commitment should be not to break things for any of these folks :)

@nicolaskruchten
Copy link
Contributor

@T4rk1n I'm a bit confused about what the component_loader's responsibility here is/when it's invoked... can you enlighten me?

@T4rk1n
Copy link
Contributor Author

T4rk1n commented Nov 29, 2018

@nicolaskruchten The component_loader is the old method for generating the components, it contains the methods for generating components at runtime and the method we were using in build:py before this PR. Keeping it for backward compatibility.

@rmarren1
Copy link
Contributor

Some history:

In the first dash iteration, component libraries would call component_loader.load_components in their __init__.py file the path to their metadata.json file. This code would generate component class strings and call exec to load them at runtime.

#276 changed this, so that that component classes were generated once at build time using component_loader.generate_components and imported properly into __init__.py.

Now this CLI will replace the component_loader.generate_components code, so everything there is for backward compatibility.

@nicolaskruchten
Copy link
Contributor

thanks for the clarifications!

💃 on my end

@T4rk1n T4rk1n merged commit 951c890 into master Nov 29, 2018
@T4rk1n T4rk1n deleted the extract-meta branch November 29, 2018 16:45
HammadTheOne pushed a commit to HammadTheOne/dash that referenced this pull request May 28, 2021
HammadTheOne pushed a commit that referenced this pull request Jul 23, 2021
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