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

[python] fixes bugs #13581

Merged
merged 3 commits into from
Oct 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/generators/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|generateSourceCodeOnly|Specifies that only a library source code is to be generated.| |false|
|hideGenerationTimestamp|Hides the generation timestamp when files are generated.| |true|
|library|library template (sub-template) to use: urllib3| |urllib3|
|nonCompliantUseDiscriminatorIfCompositionFails|When true, If the payload fails to validate against composed schemas (allOf/anyOf/oneOf/not) and a discriminator is present, then ignore the composition validation errors and attempt to use the discriminator to validate the payload.&lt;br /&gt;Note: setting this to true makes the generated client not comply with json schema because it ignores composition validation errors. Please consider making your schemas more restrictive rather than setting this to true. You can do that by:&lt;ul&gt;&lt;li&gt;defining the propertyName as an enum with only one value in the schemas that are in your discriminator map&lt;/li&gt;&lt;li&gt;setting additionalProperties: false in your schemas&lt;/li&gt;&lt;/ul&gt;|<dl><dt>**true**</dt><dd>If composition fails and a discriminator exists, the composition errors will be ignored and validation will be attempted with the discriminator</dd><dt>**false**</dt><dd>Composition validation must succeed. Discriminator validation must succeed.</dd></dl>|false|
|packageName|python package name (convention: snake_case).| |openapi_client|
|packageUrl|python package URL.| |null|
|packageVersion|python package version.| |1.0.0|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -404,4 +404,15 @@ public static enum ENUM_PROPERTY_NAMING_TYPE {camelCase, PascalCase, snake_case,

public static final String ERROR_OBJECT_TYPE = "errorObjectType";

public static final String NON_COMPLIANT_USE_DISCR_IF_COMPOSITION_FAILS = "nonCompliantUseDiscriminatorIfCompositionFails";

public static final String NON_COMPLIANT_USE_DISCR_IF_COMPOSITION_FAILS_DESC =
"When true, If the payload fails to validate against composed schemas (allOf/anyOf/oneOf/not) and a " +
"discriminator is present, then ignore the composition validation errors and attempt to use the " +
"discriminator to validate the payload.<br />" +
"Note: setting this to true makes the generated client not comply with json schema because it ignores " +
"composition validation errors. Please consider making your schemas more restrictive rather than " +
"setting this to true. You can do that by:<ul>" +
"<li>defining the propertyName as an enum with only one value in the schemas that are in your discriminator map</li>" +
"<li>setting additionalProperties: false in your schemas</li></ul>";
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ public class PythonClientCodegen extends AbstractPythonCodegen {
// for apis.tags tag api definition
private Map<String, String> tagEnumToApiClassname = new LinkedHashMap<>();

private boolean nonCompliantUseDiscrIfCompositionFails = false;

public PythonClientCodegen() {
super();
loadDeepObjectIntoItems = false;
Expand Down Expand Up @@ -215,6 +217,13 @@ public PythonClientCodegen() {
cliOptions.add(new CliOption(RECURSION_LIMIT, "Set the recursion limit. If not set, use the system default value."));
cliOptions.add(CliOption.newBoolean(USE_INLINE_MODEL_RESOLVER, "use the inline model resolver, if true inline complex models will be extracted into components and $refs to them will be used").
defaultValue(Boolean.FALSE.toString()));
CliOption nonCompliantUseDiscrIfCompositionFails = CliOption.newBoolean(CodegenConstants.NON_COMPLIANT_USE_DISCR_IF_COMPOSITION_FAILS, CodegenConstants.NON_COMPLIANT_USE_DISCR_IF_COMPOSITION_FAILS_DESC);
Map<String, String> nonCompliantUseDiscrIfCompositionFailsOpts = new HashMap<>();
nonCompliantUseDiscrIfCompositionFailsOpts.put("true", "If composition fails and a discriminator exists, the composition errors will be ignored and validation will be attempted with the discriminator");
nonCompliantUseDiscrIfCompositionFailsOpts.put("false", "Composition validation must succeed. Discriminator validation must succeed.");
nonCompliantUseDiscrIfCompositionFails.setEnum(nonCompliantUseDiscrIfCompositionFailsOpts);

cliOptions.add(nonCompliantUseDiscrIfCompositionFails);

supportedLibraries.put("urllib3", "urllib3-based client");
CliOption libraryOption = new CliOption(CodegenConstants.LIBRARY, "library template (sub-template) to use: urllib3");
Expand Down Expand Up @@ -363,6 +372,12 @@ public void processOpts() {
}
}

if (additionalProperties.containsKey(CodegenConstants.NON_COMPLIANT_USE_DISCR_IF_COMPOSITION_FAILS)) {
nonCompliantUseDiscrIfCompositionFails = Boolean.parseBoolean(
additionalProperties.get(CodegenConstants.NON_COMPLIANT_USE_DISCR_IF_COMPOSITION_FAILS).toString()
);
}

String readmePath = "README.md";
String readmeTemplate = "README." + templateExtension;
if (generateSourceCodeOnly) {
Expand Down Expand Up @@ -479,13 +494,13 @@ public String apiFilename(String templateName, String tag) {
return apiFileFolder() + File.separator + toApiFilename(tag) + suffix;
}

private void generateFiles(List<List<Object>> processTemplateToFileInfos, String skippedByOption) {
private void generateFiles(List<List<Object>> processTemplateToFileInfos, boolean shouldGenerate, String skippedByOption) {
for (List<Object> processTemplateToFileInfo: processTemplateToFileInfos) {
Map<String, Object> templateData = (Map<String, Object>) processTemplateToFileInfo.get(0);
String templateName = (String) processTemplateToFileInfo.get(1);
String outputFilename = (String) processTemplateToFileInfo.get(2);
try {
processTemplateToFile(templateData, templateName, outputFilename, true, skippedByOption);
processTemplateToFile(templateData, templateName, outputFilename, shouldGenerate, skippedByOption);
} catch (IOException e) {
LOGGER.error("Error when writing template file {}", e.toString());
}
Expand Down Expand Up @@ -649,9 +664,11 @@ protected void generateEndpoints(OperationsMap objs) {
outputFilename = packageFilename(Arrays.asList("apis", "paths", pathModule + ".py"));
apisFiles.add(Arrays.asList(operationMap, "apis_path_module.handlebars", outputFilename));
}
generateFiles(pathsFiles, CodegenConstants.APIS);
generateFiles(apisFiles, CodegenConstants.APIS);
generateFiles(testFiles, CodegenConstants.API_TESTS);
boolean shouldGenerateApis = (boolean) additionalProperties().get(CodegenConstants.GENERATE_APIS);
boolean shouldGenerateApiTests = (boolean) additionalProperties().get(CodegenConstants.GENERATE_API_TESTS);
generateFiles(pathsFiles, shouldGenerateApis, CodegenConstants.APIS);
generateFiles(apisFiles, shouldGenerateApis, CodegenConstants.APIS);
generateFiles(testFiles, shouldGenerateApiTests, CodegenConstants.API_TESTS);
}

/*
Expand Down Expand Up @@ -1024,7 +1041,9 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required, boo
cp.isNullable = false;
cp.setHasMultipleTypes(true);
}
postProcessPattern(cp.pattern, cp.vendorExtensions);
if (p.getPattern() != null) {
postProcessPattern(p.getPattern(), cp.vendorExtensions);
}
// if we have a property that has a difficult name, either:
// 1. name is reserved, like class int float
// 2. name is invalid in python like '3rd' or 'Content-Type'
Expand Down Expand Up @@ -1468,7 +1487,11 @@ protected Object processTestExampleData(Object value) {
@Override
public CodegenModel fromModel(String name, Schema sc) {
CodegenModel cm = super.fromModel(name, sc);

if (sc.getPattern() != null) {
postProcessPattern(sc.getPattern(), cm.vendorExtensions);
String pattern = (String) cm.vendorExtensions.get("x-regex");
cm.setPattern(pattern);
}
if (cm.isNullable) {
cm.setIsNull(true);
cm.isNullable = false;
Expand Down Expand Up @@ -1874,34 +1897,21 @@ private String toExampleValueRecursive(String modelName, Schema schema, Object o
example = "2";
} else if (StringUtils.isNotBlank(schema.getPattern())) {
String pattern = schema.getPattern();
List<Object> results = getPatternAndModifiers(pattern);
String extractedPattern = (String) results.get(0);
List<String> regexFlags = (List<String>) results.get(1);
/*
RxGen does not support our ECMA dialect https://github.com/curious-odd-man/RgxGen/issues/56
So strip off the leading / and trailing / and turn on ignore case if we have it
*/
Pattern valueExtractor = Pattern.compile("^/?(.+?)/?(.?)$");
Matcher m = valueExtractor.matcher(pattern);
RgxGen rgxGen = null;
if (m.find()) {
int groupCount = m.groupCount();
if (groupCount == 1) {
// only pattern found
String isolatedPattern = m.group(1);
rgxGen = new RgxGen(isolatedPattern);
} else if (groupCount == 2) {
// patterns and flag found
String isolatedPattern = m.group(1);
String flags = m.group(2);
if (flags.contains("i")) {
rgxGen = new RgxGen(isolatedPattern);
RgxGenProperties properties = new RgxGenProperties();
RgxGenOption.CASE_INSENSITIVE.setInProperties(properties, true);
rgxGen.setProperties(properties);
} else {
rgxGen = new RgxGen(isolatedPattern);
}
}
if (regexFlags.size() > 0 && regexFlags.contains("i")) {
rgxGen = new RgxGen(extractedPattern);
RgxGenProperties properties = new RgxGenProperties();
RgxGenOption.CASE_INSENSITIVE.setInProperties(properties, true);
rgxGen.setProperties(properties);
} else {
rgxGen = new RgxGen(pattern);
rgxGen = new RgxGen(extractedPattern);
}

// this seed makes it so if we have [a-z] we pick a
Expand Down Expand Up @@ -2083,12 +2093,12 @@ private String exampleForObjectModel(Schema schema, String fullPrefix, String cl
}

example += toExampleValueRecursive(propModelName,
propSchema,
propExample,
indentationLevel + 1,
propName + "=",
exampleLine + 1,
includedSchemas) + ",\n";
propSchema,
propExample,
indentationLevel + 1,
propName + "=",
exampleLine + 1,
includedSchemas) + ",\n";
}

// TODO handle additionalProperties also
Expand Down Expand Up @@ -2341,6 +2351,16 @@ protected void updatePropertyForString(CodegenProperty property, Schema p) {
property.pattern = toRegularExpression(p.getPattern());
}

@Override
public String toRegularExpression(String pattern) {
if (pattern == null) {
return null;
}
List<Object> results = getPatternAndModifiers(pattern);
String extractedPattern = (String) results.get(0);
return extractedPattern;
}

protected void updatePropertyForNumber(CodegenProperty property, Schema p) {
property.setIsNumber(true);
// float and double differentiation is determined with format info
Expand Down Expand Up @@ -2465,35 +2485,61 @@ public ModelsMap postProcessModels(ModelsMap objs) {
return postProcessModelsEnum(objs);
}

/**
* @param pattern the regex pattern
* @return List<String pattern, List<String modifer>>
*/
private List<Object> getPatternAndModifiers(String pattern) {
/*
Notes:
RxGen does not support our ECMA dialect https://github.com/curious-odd-man/RgxGen/issues/56
So strip off the leading / and trailing / and turn on ignore case if we have it

json schema test cases omit the leading and trailing /s, so make sure that the regex allows that
*/
Pattern valueExtractor = Pattern.compile("^/?(.+?)/?([simu]{0,4})$");
Matcher m = valueExtractor.matcher(pattern);
if (m.find()) {
int groupCount = m.groupCount();
if (groupCount == 1) {
// only pattern found
String isolatedPattern = m.group(1);
return Arrays.asList(isolatedPattern, null);
} else if (groupCount == 2) {
List<String> modifiers = new ArrayList<String>();
// patterns and flag found
String isolatedPattern = m.group(1);
String flags = m.group(2);
if (flags.contains("s")) {
modifiers.add("DOTALL");
}
if (flags.contains("i")) {
modifiers.add("IGNORECASE");
}
if (flags.contains("m")) {
modifiers.add("MULTILINE");
}
return Arrays.asList(isolatedPattern, modifiers);
}
}
return Arrays.asList(pattern, new ArrayList<String>());
}

/*
* The OpenAPI pattern spec follows the Perl convention and style of modifiers. Python
* does not support this in as natural a way so it needs to convert it. See
* https://docs.python.org/2/howto/regex.html#compilation-flags for details.
*/
public void postProcessPattern(String pattern, Map<String, Object> vendorExtensions) {
if (pattern != null) {
int regexLength = pattern.length();
String regex = pattern;
int i = pattern.lastIndexOf('/');
if (regexLength >= 2 && pattern.charAt(0) == '/' && i != -1) {
// json schema tests do not include the leading and trailing slashes
// so I do not think that they are required
regex = pattern.substring(1, i);
}
regex = regex.replace("'", "\\'");
List<String> modifiers = new ArrayList<String>();

if (i != -1) {
for (char c : pattern.substring(i).toCharArray()) {
if (regexModifiers.containsKey(c)) {
String modifier = regexModifiers.get(c);
modifiers.add(modifier);
}
}
}
List<Object> results = getPatternAndModifiers(pattern);
String extractedPattern = (String) results.get(0);
List<String> modifiers = (List<String>) results.get(1);

vendorExtensions.put("x-regex", regex);
vendorExtensions.put("x-modifiers", modifiers);
vendorExtensions.put("x-regex", extractedPattern);
if (modifiers.size() > 0) {
vendorExtensions.put("x-modifiers", modifiers);
}
}
}

Expand Down Expand Up @@ -2723,6 +2769,11 @@ public String sanitizeTag(String tag) {
return tag;
}

public Map<String, Object> postProcessSupportingFileData(Map<String, Object> objs) {
objs.put(CodegenConstants.NON_COMPLIANT_USE_DISCR_IF_COMPOSITION_FAILS, nonCompliantUseDiscrIfCompositionFails);
return objs;
}

@Override
public void postProcess() {
System.out.println("################################################################################");
Expand All @@ -2734,4 +2785,4 @@ public void postProcess() {
System.out.println("# Please support his work directly via https://github.com/sponsors/spacether \uD83D\uDE4F#");
System.out.println("################################################################################");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ from pprint import pprint
{{#with apiInfo}}
{{#each apis}}
{{#if @first}}
from {{packageName}}.{{apiPackage}} import {{classFilename}}
from {{packageName}}.{{apiPackage}}.tags import {{classFilename}}
{{#each imports}}
{{{import}}}
{{/each}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1047,7 +1047,7 @@ class ApiClient:
) -> urllib3.HTTPResponse:

# header parameters
headers = headers or {}
headers = headers or HTTPHeaderDict()
headers.update(self.default_headers)
if self.cookie:
headers['Cookie'] = self.cookie
Expand Down
Loading