From 61215b6d9bd868f6a5e1018bff4a6b816eb97c64 Mon Sep 17 00:00:00 2001 From: Matthew Thompson Date: Fri, 7 Jun 2024 13:42:03 -0400 Subject: [PATCH 1/2] Allow alternative mangle for LONG_NAME (R21C) --- Apps/MAPL_GridCompSpecs_ACG.py | 364 ++++++++++++++++++++------------- CHANGELOG.md | 6 + cmake/mapl_acg.cmake | 12 +- 3 files changed, 235 insertions(+), 147 deletions(-) diff --git a/Apps/MAPL_GridCompSpecs_ACG.py b/Apps/MAPL_GridCompSpecs_ACG.py index 0d2a80f19045..13d113da3787 100755 --- a/Apps/MAPL_GridCompSpecs_ACG.py +++ b/Apps/MAPL_GridCompSpecs_ACG.py @@ -9,49 +9,137 @@ from enum import Enum +################################# CONSTANTS #################################### SUCCESS = 0 - CATEGORIES = ("IMPORT","EXPORT","INTERNAL") +LONGNAME_GLOB_PREFIX = "longname_glob_prefix" +# constants for logicals +TRUE_VALUE = '.true.' +FALSE_VALUE = '.false.' +TRUE_VALUES = {'t', 'true', 'yes', 'y', 'si', 'oui', 'sim'} +FALSE_VALUES = {'f', 'false', 'no', 'n', 'no', 'non', 'nao'} + +# constants used for Option.DIMS and computing rank +DIMS_OPTIONS = [('MAPL_DimsVertOnly', 1, 'z'), ('MAPL_DimsHorzOnly', 2, 'xy'), ('MAPL_DimsHorzVert', 3, 'xyz')] +RANKS = dict([(entry, rank) for entry, rank, _ in DIMS_OPTIONS]) + + +############################### HELPER FUNCTIONS ############################### +def make_string_array(s): + """ Returns a string representing a Fortran character array """ + rm_quotes = lambda s: s.strip().strip('"\'').strip() + add_quotes = lambda s: "'" + s + "'" + ss = s.strip() + if ',' in ss: + ls = [s.strip() for s in s.strip().split(',')] + else: + ls = s.strip().split() + ls = [rm_quotes(s) for s in ls] + ls = [s for s in ls if s] + n = max(ls) + ss = ','.join([add_quotes(s) for s in ls]) + return f"[character(len={n}) :: {ss}]" + +def make_entry_emit(dictionary): + """ Returns a emit function that looks up the value in dictionary """ + return lambda key: dictionary[key] if key in dictionary else None + +def mangle_name_prefix(name, parameters = None): + pre = 'comp_name' + if isinstance(parameters, tuple): + pre = parameters[0] if parameters[0] else pre + codestring = f"'//trim({pre})//'" + return string_emit(name.replace("*",codestring)) if name else None + +def get_fortran_logical(value_in): + """ Return string representing Fortran logical from an input string """ + """ representing a logical value input """ + + try: + if value_in is None: + raise ValueError("'None' is not valid for get_fortran_logical.") + if value_in.strip().lower() in TRUE_VALUES: + val_out = TRUE_VALUE + elif value_in.strip().lower() in FALSE_VALUES: + val_out = FALSE_VALUE + else: + raise ValueError("Unrecognized logical: " + value_in) + except Exception: + raise + + return val_out + +def compute_rank(dims, ungridded): + extra_rank = len(ungridded.strip('][').split(',')) if ungridded else 0 + return RANKS[dims] + extra_rank -"""Helper functions -lambda (anonymous) functions are simple functions (usually one line), -of the form: - lambda x, y, z, ...: -where 'x, y, z, ...' represents one or more arguments (It's a tuple.) -They are quite handy for processing sequences (think: list, tuples, sets) -They are used here for emitting values, as well. -""" +def header(): + """ + Returns a standard warning that can be placed at the top of each + generated _Fortran_ include file. + """ + + return """ +! ------------------- +! W A R N I N G +! ------------------- +! +! This code fragment is automatically generated by MAPL_GridCompSpecs_ACG. +! Please DO NOT edit it. Any modification made in here will be overwritten +! next time this file is auto-generated. Instead, enter your additions +! or deletions in the .rc file in the src tree. +! + """ +def open_with_header(filename): + f = open(filename,'w') + f.write(header()) + return f + +# callable object (function) +class ParameterizedEmitFunction: + + def __init__(self, emit, *parameter_keys): + self.emit = emit + self.parameter_keys = parameter_keys + + def __call__(self, name, parameters): + parameter_values = tuple(parameters.get(key) for key in self.parameter_keys) + return self.emit(name, parameter_values) + + +##################### EMIT functions for writing AddSpecs ###################### # Return the value identity_emit = lambda value: value # Return value in quotes string_emit = lambda value: ("'" + value + "'") if value else None # Return value in brackets array_emit = lambda value: ('[' + value + ']') if value else None - +# Strip '.' and ' ' [SPACE] +lstripped = lambda s: s.lower().strip(' .') +# emit function for character arrays +string_array_emit = lambda value: make_string_array(value) if value else None +# mangle name for SHORT_NAME mangle_name = lambda name: string_emit(name.replace("*","'//trim(comp_name)//'")) if name else None +# mangle name for internal use make_internal_name = lambda name: name.replace('*','') if name else None - -def make_entry_emit(dictionary): - """ Returns a emit function that looks up the value in dictionary """ - return lambda key: dictionary[key] if key in dictionary else None - -# constants used for Option.DIMS -DIMS_OPTIONS = [('MAPL_DimsVertOnly', 1, 'z'), ('MAPL_DimsHorzOnly', 2, 'xy'), ('MAPL_DimsHorzVert', 3, 'xyz')] +# emit function for LONG_NAME +mangle_longname = ParameterizedEmitFunction(mangle_name_prefix, LONGNAME_GLOB_PREFIX) +# emit for function for DIMS DIMS_EMIT = make_entry_emit(dict([(alias, entry) for entry, _, alias in DIMS_OPTIONS])) -RANKS = dict([(entry, rank) for entry, rank, _ in DIMS_OPTIONS]) - # emit function for Option.VLOCATION VLOCATION_EMIT = make_entry_emit({'C': 'MAPL_VlocationCenter', 'E': 'MAPL_VlocationEdge', 'N': 'MAPL_VlocationNone'}) - +# emit function for Option.ADD2EXPORT +ADD2EXPORT_EMIT = make_entry_emit({'T': '.true.', 'F': '.false.'}) +# emit function for logical-valued options +logical_emit = lambda s: TRUE_VALUE if lstripped(s) in TRUE_VALUES else FALSE_VALUE if lstripped(s) in FALSE_VALUES else None # emit function for Option.RESTART RESTART_EMIT = make_entry_emit({'OPT' : 'MAPL_RestartOptional', 'SKIP' : 'MAPL_RestartSkip', 'REQ' : 'MAPL_RestartRequired', 'BOOT' : 'MAPL_RestartBoot', 'SKIPI': 'MAPL_RestartSkipInitial'}) -# emit function for Option.ADD2EXPORT -ADD2EXPORT_EMIT = make_entry_emit({'T': '.true.', 'F': '.false.'}) +################################### OPTIONS #################################### # parent class for class Option # defines a few methods class OptionType(Enum): @@ -75,8 +163,8 @@ def get_mandatory_options(cls): 'SHORT_NAME': ('short_name', mangle_name, True), 'NAME': ('short_name', mangle_name, True), 'DIMS': ('dims', DIMS_EMIT, True), - 'LONG_NAME': ('long_name', mangle_name, True), - 'LONG NAME': ('long_name', mangle_name, True), + 'LONG_NAME': ('long_name', mangle_longname, True), + 'LONG NAME': ('long_name', mangle_longname, True), 'UNITS': ('units', string_emit, True), # OPTIONAL 'ADD2EXPORT': ('add2export', ADD2EXPORT_EMIT), @@ -89,6 +177,8 @@ def get_mandatory_options(cls): 'AVINT': ('averaging_interval',), 'DATATYPE': ('datatype',), 'DEFAULT': ('default',), + 'DEPENDS_ON_CHILDREN': ('depends_on_children', logical_emit), + 'DEPENDS_ON': ('depends_on', string_array_emit), 'FIELD_TYPE': ('field_type',), 'FRIENDLYTO': ('friendlyto', string_emit), 'FRIEND2': ('friendlyto', string_emit), @@ -118,6 +208,9 @@ def get_mandatory_options(cls): 'RANK': ('rank', None, False, False) }, type = OptionType) + +###################### RULES to test conditions on Options ##################### +# relations for rules on Options def relation(relop, lhs, rhs, values): """ Returns the result of the relop relation of lhs and rhs using values for lookups """ l = values[lhs] if isinstance(lhs, Option) else lhs @@ -175,6 +268,7 @@ def check(self, values): """ run rules on Option values """ return self.rule(values) +# These are the CURRENT RULES of Option (column) values def check_option_values(values): rules = [Rule(conditions = [(Option.DIMS, equals, 'MAPL_DimsHorzVert', 'is equal to MAPL_DimsHorzVert'), @@ -185,51 +279,9 @@ def check_option_values(values): for rule in rules: rule.check(values) -def compute_rank(dims, ungridded): - extra_rank = len(ungridded.strip('][').split(',')) if ungridded else 0 - return RANKS[dims] + extra_rank - -def digest(specs): - """ Set Option values from parsed specs """ - mandatory_options = Option.get_mandatory_options() - digested_specs = dict() - - for category in specs: - category_specs = list() # All the specs for the category - for spec in specs[category]: # spec from list - dims = None - ungridded = None - option_values = dict() # dict of option values - for column in spec: # for spec emit value - column_value = spec[column] - option = Option[column.upper()] # use column name to find Option - option_value = option(column_value) # emit value - option_values[option] = option_value # add value to dict - if option == Option.SHORT_NAME: - option_values[Option.MANGLED_NAME] = Option.MANGLED_NAME(column_value) - option_values[Option.INTERNAL_NAME] = Option.INTERNAL_NAME(column_value) - elif option == Option.DIMS: - dims = option_value - elif option == Option.UNGRIDDED: - ungridded = option_value -# MANDATORY - for option in mandatory_options: - if option not in option_values: - raise RuntimeError(option.name + " is missing from spec.") -# END MANDATORY - option_values[Option.RANK] = compute_rank(dims, ungridded) -# CHECKS HERE - try: - check_option_values(option_values) - except Exception: - raise -# END CHECKS - category_specs.append(option_values) - digested_specs[category] = category_specs - return digested_specs - ############################################################### +# MAPL_DATASPEC class class MAPL_DataSpec: """Declare and manipulate an import/export/internal specs for a MAPL Gridded component""" @@ -325,6 +377,36 @@ def emit_trailer(self, nullify=False): text = self.newline() return text + +############################ PARSE COMMAND ARGUMENTS ########################### +def get_args(): + parser = argparse.ArgumentParser(description='Generate import/export/internal specs for MAPL Gridded Component') + parser.add_argument("input", action='store', + help="input filename") + parser.add_argument("-n", "--name", action="store", + help="override default grid component name derived from input filename") + parser.add_argument("-i", "--import_specs", action="store", nargs='?', + default=None, const="{component}_Import___.h", + help="override default output filename for AddImportSpec() code") + parser.add_argument("-x", "--export_specs", action="store", nargs='?', + default=None, const="{component}_Export___.h", + help="override default output filename for AddExternalSpec() code") + parser.add_argument("-p", "--internal_specs", action="store", nargs='?', + default=None, const="{component}_Internal___.h", + help="override default output filename for AddImportSpec() code") + parser.add_argument("-g", "--get-pointers", action="store", nargs='?', + default=None, const="{component}_GetPointer___.h", + help="override default output filename for get_pointer() code") + parser.add_argument("-d", "--declare-pointers", action="store", nargs='?', + const="{component}_DeclarePointer___.h", default=None, + help="override default output filename for pointer declaration code") + parser.add_argument("--" + LONGNAME_GLOB_PREFIX, dest=LONGNAME_GLOB_PREFIX, + action="store", nargs='?', default=None, + help="alternative prefix for long_name substitution") + return parser.parse_args() + + +# READ_SPECS function def read_specs(specs_filename): """Returns dict of (category: list of dict of (option name: option value) """ def csv_record_reader(csv_reader): @@ -368,88 +450,56 @@ def dataframe(reader, columns): return specs -def get_fortran_logical(value_in): - """ Return string representing Fortran logical from an input string """ - """ representing a logical value input """ - TRUE_VALUE = '.true.' - FALSE_VALUE = '.false.' - TRUE_VALUES = {TRUE_VALUE, 't', 'true', '.t.', 'yes', 'y', 'si', 'oui', 'sim'} - FALSE_VALUES = {FALSE_VALUE, 'f', 'false', '.f.', 'no', 'n', 'no', 'non', 'nao'} - - try: - if value_in is None: - raise ValueError("'None' is not valid for get_fortran_logical.") - if value_in.strip().lower() in TRUE_VALUES: - val_out = TRUE_VALUE - elif value_in.strip().lower() in FALSE_VALUES: - val_out = FALSE_VALUE - else: - raise ValueError("Unrecognized logical: " + value_in) - except Exception: - raise - - return val_out - -def header(): - """ - Returns a standard warning that can be placed at the top of each - generated _Fortran_ include file. - """ - - return """ -! ------------------- -! W A R N I N G -! ------------------- -! -! This code fragment is automatically generated by MAPL_GridCompSpecs_ACG. -! Please DO NOT edit it. Any modification made in here will be overwritten -! next time this file is auto-generated. Instead, enter your additions -! or deletions in the .rc file in the src tree. -! - """ - -def open_with_header(filename): - f = open(filename,'w') - f.write(header()) - return f -############################################# -# Main program begins here -############################################# - -if __name__ == "__main__": +# DIGEST +def digest(specs, args): + """ Set Option values from parsed specs """ + arg_dict = vars(args) + mandatory_options = Option.get_mandatory_options() + digested_specs = dict() -# Process command line arguments - parser = argparse.ArgumentParser(description='Generate import/export/internal specs for MAPL Gridded Component') - parser.add_argument("input", action='store', - help="input filename") - parser.add_argument("-n", "--name", action="store", - help="override default grid component name derived from input filename") - parser.add_argument("-i", "--import_specs", action="store", nargs='?', - default=None, const="{component}_Import___.h", - help="override default output filename for AddImportSpec() code") - parser.add_argument("-x", "--export_specs", action="store", nargs='?', - default=None, const="{component}_Export___.h", - help="override default output filename for AddExternalSpec() code") - parser.add_argument("-p", "--internal_specs", action="store", nargs='?', - default=None, const="{component}_Internal___.h", - help="override default output filename for AddImportSpec() code") - parser.add_argument("-g", "--get-pointers", action="store", nargs='?', - default=None, const="{component}_GetPointer___.h", - help="override default output filename for get_pointer() code") - parser.add_argument("-d", "--declare-pointers", action="store", nargs='?', - const="{component}_DeclarePointer___.h", default=None, - help="override default output filename for pointer declaration code") - args = parser.parse_args() + for category in specs: + category_specs = list() # All the specs for the category + for spec in specs[category]: # spec from list + dims = None + ungridded = None + option_values = dict() # dict of option values + for column in spec: # for spec emit value + column_value = spec[column] + option = Option[column.upper()] # use column name to find Option + # emit value + if type(option.emit) is ParameterizedEmitFunction: + option_value = option.emit(column_value, arg_dict) + else: + option_value = option.emit(column_value) + option_values[option] = option_value # add value to dict + if option == Option.SHORT_NAME: + option_values[Option.MANGLED_NAME] = Option.MANGLED_NAME(column_value) + option_values[Option.INTERNAL_NAME] = Option.INTERNAL_NAME(column_value) + elif option == Option.DIMS: + dims = option_value + elif option == Option.UNGRIDDED: + ungridded = option_value +# MANDATORY + for option in mandatory_options: + if option not in option_values: + raise RuntimeError(option.name + " is missing from spec.") +# END MANDATORY + option_values[Option.RANK] = compute_rank(dims, ungridded) +# CHECKS HERE + try: + check_option_values(option_values) + except Exception: + raise +# END CHECKS + category_specs.append(option_values) + digested_specs[category] = category_specs -# Process blocked CSV input file - parsed_specs = read_specs(args.input) + return digested_specs + -# Emit values - try: - specs = digest(parsed_specs) - except Exception: - raise +################################# EMIT_VALUES ################################## +def emit_values(specs, args): if args.name: component = args.name @@ -498,5 +548,27 @@ def open_with_header(filename): if f_get_pointers: f_get_pointers.close() - sys.exit(SUCCESS) +############################################# +# MAIN program begins here +############################################# + +if __name__ == "__main__": +# Process command line arguments + args = get_args() + +# Process blocked CSV input file + parsed_specs = read_specs(args.input) + +# Digest specs from file to output structure + try: + specs = digest(parsed_specs, args) + + except Exception: + raise + +# Emit values + emit_values(specs, args) + +# FIN + sys.exit(SUCCESS) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22ae8a1e7f87..ab05ad50df53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Deprecated +## [v2.35.3+R21C_v1.2.0] - 2024-06-XX + +### Changed + +- Add capability to mangle `LONG_NAME in ACG with a different prefix + ## [v2.35.3+R21C_v1.1.0] - 2024-03-28 ### Fixed diff --git a/cmake/mapl_acg.cmake b/cmake/mapl_acg.cmake index fe125c68770b..0197f500a55b 100644 --- a/cmake/mapl_acg.cmake +++ b/cmake/mapl_acg.cmake @@ -12,6 +12,10 @@ # INTERNAL_SPECS [file] include file for AddInternalSpec() code (default _Internal___.h) # GET_POINTERS [file] include file for GetPointer() code (default _GetPointer___.h) # DECLARE_POINTERS [file] include file for declaring local pointers (default _DeclarePointer___.h) +# LONG_NAME_PREFIX [string] prefix for long names (default "comp_name") +# +# NOTE: Use of LONG_NAME_PREFIX will require changes to the Fortran code as all the ACG does +# is write Fortran. So, you'll need to define a string in the Fortran for this # ################################################################################################ @@ -22,7 +26,7 @@ function (mapl_acg target specs_file) # This list must align with oneValueArgs above (for later ZIP_LISTS) set (flags -i -x -p -g -d) set (defaults Import Export Internal GetPointer DeclarePointer) - set (multiValueArgs) + set (multiValueArgs LONG_NAME_PREFIX) cmake_parse_arguments (ARGS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) string (REPLACE "_GridComp" "" component_name ${target}) @@ -35,6 +39,12 @@ function (mapl_acg target specs_file) set (options "") set (suffix_for_generated_include_files "___.h") + # Note: Use the equals sign below. If a space is used, CMake did + # weird things + if (ARGS_LONG_NAME_PREFIX) + list (APPEND options "--longname_glob_prefix=${ARGS_LONG_NAME_PREFIX}") + endif () + # Handle oneValueArgs with no value (Python provides default) foreach (opt flag default IN ZIP_LISTS oneValueArgs flags defaults) From 1c5fc7ac90d49f9ce094a020631a613bf15f56eb Mon Sep 17 00:00:00 2001 From: Matthew Thompson Date: Fri, 7 Jun 2024 17:13:28 -0400 Subject: [PATCH 2/2] Fix up CI --- .github/workflows/changelog-enforcer.yml | 4 ++-- .github/workflows/enforce-labels.yml | 8 ++++---- .github/workflows/validate_yaml_files.yml | 4 ++-- .github/workflows/workflow.yml | 8 ++++---- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/changelog-enforcer.yml b/.github/workflows/changelog-enforcer.yml index 043ac2ee81f6..f7df2f3f97b5 100644 --- a/.github/workflows/changelog-enforcer.yml +++ b/.github/workflows/changelog-enforcer.yml @@ -11,12 +11,12 @@ jobs: - uses: dangoslen/changelog-enforcer@v3 with: changeLogPath: 'CHANGELOG.md' - skipLabels: 'Skip Changelog,0 diff trivial,automatic,dependencies,github_actions' + skipLabels: "Changelog Skip,0 Diff Trivial,:wrench: Github Actions" missingUpdateErrorMessage: > No update to CHANGELOG.md found! Please add a changelog entry to it describing your change. Please note that the keepachangelog (https://keepachangelog.com) format is used. If your change is very trivial not applicable for a - changelog entry, add a 'Skip Changelog' label to the pull + changelog entry, add a 'Changelog Skip' label to the pull request to skip the changelog enforcer. diff --git a/.github/workflows/enforce-labels.yml b/.github/workflows/enforce-labels.yml index 238e6c6c5cde..d9dc79b76f2a 100644 --- a/.github/workflows/enforce-labels.yml +++ b/.github/workflows/enforce-labels.yml @@ -8,25 +8,25 @@ jobs: require-label: runs-on: ubuntu-latest steps: - - uses: mheap/github-action-required-labels@v3 + - uses: mheap/github-action-required-labels@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: mode: minimum count: 1 - labels: "0 diff,0 diff trivial,Non 0-diff,0 diff structural,0-diff trivial,Not 0-diff,0-diff,automatic,0-diff uncoupled,github_actions" + labels: "0 diff,0 diff trivial,:astonished: Non 0 Diff,:wrench: Github Actions" add_comment: true message: "This PR is being prevented from merging because you have not added one of our required labels: {{ provided }}. Please add one so that the PR can be merged." blocking-label: runs-on: ubuntu-latest steps: - - uses: mheap/github-action-required-labels@v3 + - uses: mheap/github-action-required-labels@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: mode: exactly count: 0 - labels: "Contingent - DNA,Needs Lead Approval,Contingent -- Do Not Approve" + labels: ":no_entry_sign: Contingent - DNA" add_comment: true message: "This PR is being prevented from merging because you have added one of our blocking labels: {{ provided }}. You'll need to remove it before this PR can be merged." diff --git a/.github/workflows/validate_yaml_files.yml b/.github/workflows/validate_yaml_files.yml index 86df2bb00b7c..449db6e674da 100644 --- a/.github/workflows/validate_yaml_files.yml +++ b/.github/workflows/validate_yaml_files.yml @@ -15,7 +15,7 @@ jobs: validate-YAML: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3.3.0 + - uses: actions/checkout@v4 - id: yaml-lint name: yaml-lint uses: ibiqlik/action-yamllint@v3 @@ -24,7 +24,7 @@ jobs: format: colored config_file: .yamllint.yml - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: always() with: name: yamllint-logfile diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index ac6ebd4acf39..dce9b3e26cc8 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -30,11 +30,11 @@ jobs: OMPI_MCA_btl_vader_single_copy_mechanism: none steps: - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.11.0 + uses: styfle/cancel-workflow-action@0.12.1 with: access_token: ${{ github.token }} - name: Checkout - uses: actions/checkout@v3.3.0 + uses: actions/checkout@v4 with: fetch-depth: 1 - name: Set all directories as git safe @@ -86,11 +86,11 @@ jobs: #password: ${{ secrets.DOCKERHUB_TOKEN }} steps: - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.11.0 + uses: styfle/cancel-workflow-action@0.12.1 with: access_token: ${{ github.token }} - name: Checkout - uses: actions/checkout@v3.3.0 + uses: actions/checkout@v4 with: fetch-depth: 1 - name: Set all directories as git safe