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

OID as names and multiple modules #72

Open
wants to merge 8 commits into
base: master
Choose a base branch
from

Conversation

vpiserchia
Copy link

Allow user to specify two new arguments

  • --skip-missing: will ignore missing identifier not defined in the current module
  • --use-oid: use OID for creating the resulting file. ONLY in conjuction of --split

Together these two new switch will allow working with multiple modules using a coherent naming schema

- --skip-missing: will ignore missing identifier not defined in the current module
- --use-oid: use OID for creating the resulting file. ONLY in conjuction of --split

Together these two new switch will allow working with multiple modules using a coherent naming schema
Copy link
Owner

@kimgr kimgr left a comment

Choose a reason for hiding this comment

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

Thank you for this!

Can I ask that you split the two changes into discrete commits? The --skip-missing in one and the --use-oid in one (unless they're interdependent, but it doesn't look like it).

Some more review feedback inline.


for imp, values in self.sema_module.imports.imports.iteritems():
self.writer.write_line('from _%s import %s' %
('_'.join([str(d.number) for d in imp.descendants() if isinstance(d, NameAndNumberForm)]),', '.join(map(_sanitize_identifier, values))))
Copy link
Owner

Choose a reason for hiding this comment

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

Please use two temporaries for these expressions;

for imp, values in self.sema_module.imports.imports.iteritems():
    module = '_'.join([str(d.number) for d in imp.descendants() if isinstance(d, NameAndNumberForm)])
    symbols = ', '.join(map(_sanitize_identifier, values))))
    self.writer.write_line('from _%s import %s' % (module, symbols)

This comment was marked as resolved.


for imp, values in self.sema_module.imports.imports.iteritems():
self.writer.write_line('from _%s import %s' %
('_'.join([str(d.number) for d in imp.descendants() if isinstance(d, NameAndNumberForm)]),', '.join(map(_sanitize_identifier, values))))
Copy link
Owner

Choose a reason for hiding this comment

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

No need for list literal inside join([...]), join works on a generator expression too.

Copy link
Author

Choose a reason for hiding this comment

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

+1


for imp, values in self.sema_module.imports.imports.iteritems():
self.writer.write_line('from _%s import %s' %
('_'.join([str(d.number) for d in imp.descendants() if isinstance(d, NameAndNumberForm)]),', '.join(map(_sanitize_identifier, values))))
Copy link
Owner

Choose a reason for hiding this comment

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

I tend to prefer generator expressions over map, but I guess that expression becomes more complicated as join(_sanitize_identifier(v) for v in values). Actually, it doesn't look so bad, let's use that.

Copy link
Author

Choose a reason for hiding this comment

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

+1

@@ -129,6 +129,10 @@ def __init__(self, sema_module, out_stream, referenced_modules):
def generate_code(self):
self.writer.write_line('# %s' % self.sema_module.name)
self.writer.write_line('from pyasn1.type import univ, char, namedtype, namedval, tag, constraint, useful')

for imp, values in self.sema_module.imports.imports.iteritems():
Copy link
Owner

Choose a reason for hiding this comment

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

Does this even work without --use-oid?


for imp, values in self.sema_module.imports.imports.iteritems():
self.writer.write_line('from _%s import %s' %
('_'.join([str(d.number) for d in imp.descendants() if isinstance(d, NameAndNumberForm)]),', '.join(map(_sanitize_identifier, values))))
for module in self.referenced_modules:
Copy link
Owner

Choose a reason for hiding this comment

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

Seeing this, I'm wondering why the OID-named module is not part of self.referenced_modules... Would it be possible to fold it into the same infrastructure? I don't remember the design here, so it might be impossible/messy, but it looks like it might fit better there.

Copy link
Author

Choose a reason for hiding this comment

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

need to make some experiments

Copy link
Author

Choose a reason for hiding this comment

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

I haven't found a quick way to solve this, help needed here

Copy link
Owner

Choose a reason for hiding this comment

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

I looked through this, and it seems like referenced_modules is the "module corpus" being defined by the ASN.1 input (i.e. there can be more than one module definition in the same input file).

So prior to your changes, we would only allow imports and type resolution between fully defined modules.

The pyasn1 generation would also generate a distinct Python module for each ASN.1 module. Since the ASN.1 was free to refer to types and values using e.g. Module2.MyType from Module1.MyConstructedType, the generated Python would add imports of all modules in the "corpus", so they were available.

Now you're trying to introduce something similar for IMPORTS. I think the difference is that there's not necessarily any ASN.1 definition available to asn1ate for an imported construct (there would need to exist some way to map an ASN.1 module ref to an external ASN.1 file, which would also need to be parsed, etc. None of this currently exists).

So I'm a little curious how well this use_oid flag generalizes. What happens for the existing tests in testdata/imports.asn? My guess is we will codegen imports of OID-named Python modules that don't exist (e.g. _2_5_1_0_4.py for ImportMultipleWithAssignedOID in that test file).

And not all IMPORTS are NameAndNumberForm, so the module names will not necessarily be complete/correct.

Hmm. This is all pretty complex, which is probably why I never got around to implementing it properly.

Copy link
Owner

Choose a reason for hiding this comment

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

It seems to me your changes must work for some particular case -- do you have a (possibly anonymized) ASN.1 input you could share that requires these changes and for which the generated pyasn1 is functional?

Copy link
Author

Choose a reason for hiding this comment

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

my use case is to allow building a module without this having all the declaration available in the same file; now I see I misused the tool and this creates more problems than it solves (for my lack of knowledge). That said I think it's better to close this PR, unless you see anything useful.

Copy link
Owner

Choose a reason for hiding this comment

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

I'm thinking something must have gone right :). I like the use case of half-translating ASN.1 and leave some manual fixup to the user. It's much easier than doing it all by hand. But we want to make sure we don't silently generate something that's broken or make asn1ate more self-inconsistent.

@@ -668,7 +672,10 @@ def main(args):

for module in modules:
if args.split:
outfile = _sanitize_module(module.name) + '.py'
if args.use_oid:
outfile = "_" + "_".join(module.oid) + '.py'
Copy link
Owner

Choose a reason for hiding this comment

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

Add a _sanitize_oid?

Copy link
Author

Choose a reason for hiding this comment

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

+1

@@ -694,6 +701,10 @@ def main_cli():
help='output multiple modules to separate files')
arg_parser.add_argument('--include-asn1', action='store_true',
help='output ASN.1 source as part of generated code')
arg_parser.add_argument('--use-oid', action='store_true',
help='output module using the OID')
Copy link
Owner

Choose a reason for hiding this comment

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

More like name output module using the OID, right?

Copy link
Author

Choose a reason for hiding this comment

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

+1

asn1ate/sema.py Outdated
""" Build a semantic model of the ASN.1 definition
from a syntax tree generated by asn1ate.parser.
"""
global skip_missing
Copy link
Owner

Choose a reason for hiding this comment

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

I'm not too happy about the global, but I can't think of a nice way to trickle the flag down all the way to where it's used. 🤷, it's fine, I guess.

Copy link
Owner

Choose a reason for hiding this comment

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

Actually, since it's only used in resolve_type_decl, why not make it an argument to that function instead, and keep it as an instance member in the Pyasn1Backend?

Copy link
Author

Choose a reason for hiding this comment

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

instance member is fine, I'll fix in the next version of the PR

Copy link
Author

Choose a reason for hiding this comment

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

resolve_type_decl is part of the sema.py not defined in Pyasn1Backend, cannot find a better way to accomplish this

Copy link
Owner

@kimgr kimgr Jan 10, 2021

Choose a reason for hiding this comment

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

# sema.py
class Module(SemaNode):
   ...
   def resolve_type_decl(self, type_decl, referenced_modules, skip_missing=False):
       ...
# pyasn1gen.py
class Pyasn1Backend(object):
...
    def __init__(self, ..., skip_missing=False):
        self.skip_missing = skip_missing

    def some_other_method(self):
        self.sema_module.resolve_type_decl(..., self.skip_missing)

def generate_pyasn1(sema_module, out_stream, referenced_modules, header=None, footer=None, skip_missing=False):
  ...

asn1ate/sema.py Outdated
@@ -302,6 +307,12 @@ def resolve_type_decl(self, type_decl, referenced_modules):
raise Exception('Unrecognized referenced module %s in %s.' % (type_decl.module_ref.name,
[module.name for module in
referenced_modules]))

if type_decl.type_name not in module.user_types():
Copy link
Owner

Choose a reason for hiding this comment

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

So what are the semantics of --skip-missing? Maybe it would be nice to describe it in a comment here.

It looks a little dangerous to return None here, because there are several places in pyasn1gen where the resolved type is used without None- or type checking.

Copy link
Author

Choose a reason for hiding this comment

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

+1 for comment.

please suggest a safer return value

Copy link
Owner

Choose a reason for hiding this comment

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

I'm not sure there's a safer value as such -- the problem is that all callers need to have a way to deal with a missing type here now. Search for resolve_type_decl in pyasn1gen.py. I don't know what a code generator should do if the semantic model has holes in it like this.

asn1ate/sema.py Outdated

if type_decl.type_name not in module.user_types():
if skip_missing:
for k, value in self.imports.imports.items():
Copy link
Owner

Choose a reason for hiding this comment

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

Particularly, I don't understand this part. We only skip it if it's part of the imports?

Copy link
Author

Choose a reason for hiding this comment

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

that's the idea, we skip IFF the declared type was in the IMPORTS section of the ASN.1 schema otherwise it will keep the same behaviour as before, e.g. raising an KeyError exception.

Copy link
Owner

@kimgr kimgr Jan 10, 2021

Choose a reason for hiding this comment

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

I don't really understand why, though. It's been a couple of years since I had my head in ASN.1, so I'm a little rusty...

I can see that non-imported types (defined in the same module) can never be missing. But in a correct multi-module schema, the imported ones should never be missing either, right? If a comment would say # HACK: Can't figure out imported symbols so ignore them for now that would tell one story. If it would say # It's OK for imported symbols to be missing because XYZ, it would be easier to understand depending on what XYZ says :)

Copy link
Author

Choose a reason for hiding this comment

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

maybe this would solve a use case like: no correct multi-module schema is available and the user still wants to generate a code that he can after manually change to resolve the imports?

Copy link
Owner

Choose a reason for hiding this comment

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

Yeah, I think that makes sense. I wonder if you could substitute something like ASN.1 ANY to indicate that there's a hole in the schema, and print a warning that the original type is unknown.

Copy link
Author

Choose a reason for hiding this comment

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

what I can think of is to collect together all the missing referenced identifier (from all the modules in the corpus) and add them to the end of the generated output; e.g. if IPAddress is referenced in the schema but imported from a missing ASN.1 (let's call it missingschema) , then asn1ate could generate:

try:
  from missingschema import IPAddress
except ImportError:
  from pyasn1.type import univ
  def  IPAddress(univ.Any):
    pass

Copy link
Owner

Choose a reason for hiding this comment

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

Shouldn't you be able to know that missingschema is unavailable already at codegen time? Doesn't seem very useful to force an ImportError. But on the other hand it's actually a nice visual indicator in the generated code that something is off.

Copy link
Author

Choose a reason for hiding this comment

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

sorry for the delay, I had some urgencies..
did you make your mind clear about this?

Copy link
Owner

Choose a reason for hiding this comment

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

No, I've also been busy with other things. It would be nice to just conclude on something, but I think it takes some iteration to figure out what the best solution is.

@vpiserchia
Copy link
Author

I'll wait for your comments where requested and going to close this PR for a v2.

@kimgr
Copy link
Owner

kimgr commented Jan 10, 2021

I'll wait for your comments where requested and going to close this PR for a v2.

Don't close it; just rewrite the branch in place and force-push to the existing PR branch using git push -f.

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.

None yet

3 participants