Skip to content

Commit

Permalink
feat: remove prompt from python stack
Browse files Browse the repository at this point in the history
This commit removes the prompt to the user
asking what Python version the project uses
if it can't be detected.

To achieve this, this commit also adds a
function in the context to emit errors from
stack plugins and allow an introspection to
return undefined. The undefined return should
be interpreted as an failure to introspect
the stack.

ref #11
  • Loading branch information
oesgalha committed Aug 25, 2021
1 parent 53cdfd0 commit edd8197
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 36 deletions.
8 changes: 6 additions & 2 deletions plugins/stack/python/mod.ts
Expand Up @@ -8,10 +8,10 @@ export default interface PythonProject {
/**
* Python version
*/
version: string;
version?: string;
}

export const introspector: Introspector<PythonProject> = {
export const introspector: Introspector<PythonProject | undefined> = {
detect: async (context) => {
return await context.files.includes("**/*.py");
},
Expand All @@ -21,6 +21,10 @@ export const introspector: Introspector<PythonProject> = {
// Version
logger.debug("detecting version");
const version = await introspectVersion(context);
if (version === undefined) {
logger.debug("didn't detect the version");
return undefined;
}
logger.debug(`detected version ${version}`);
return {
version: version,
Expand Down
68 changes: 40 additions & 28 deletions plugins/stack/python/version.ts
@@ -1,23 +1,39 @@
import { IntrospectFn } from "../deps.ts";

// Used when the Python version can't be introspected
const QUESTION =
"Couldn't determine the Python version. Please select an option";
// Offer current available Python versions, check those here:
// https://devguide.python.org/#status-of-python-branches
const PYTHON_VERSIONS = [
"3.6",
"3.7",
"3.8",
"3.9",
];

export const introspect: IntrospectFn<string> = async (context) => {
// If there is user defined configuration, use that value
if (context.config.plugins.python?.version) {
return context.config.plugins.python?.version;
}
const ERR_UNDETECTABLE_TITLE =
"Couldn't detect which Python version this project uses.";
const ERR_UNDETECTABLE_INSTRUCTIONS = `
To fix this issue, consider one of the following suggestions:
1. Adopt Pipenv
Pipenv is a tool, which is maintaned by the Python Packaging Authority, that
manages project dependencies, a local virtualenv, split dependencies between
development and production, and declares what is the Python version used in
the project.
See https://pipenv.pypa.io/
2. Adopt Poetry
Poetry is a popular alternative to Pipenv, it solves similar problems and helps
to build and publish Python packages. It also declares what Python version a
project is using.
See https://python-poetry.org/
3. Create a .python-version file
The .python-version file is used by pyenv to choose a specific Python version
for a project.
Its the easiest option, all you have to do is create a .python-version text
file with a version inside, like "3.9".
See https://github.com/pyenv/pyenv
`;

export const introspect: IntrospectFn<string | undefined> = async (context) => {
// Search for application specific `.python-version` file from pyenv
//
// See https://github.com/pyenv/pyenv/#choosing-the-python-version
Expand All @@ -44,19 +60,15 @@ export const introspect: IntrospectFn<string> = async (context) => {
const version: string | null = pyproject?.tool?.poetry?.dependencies
?.python;
if (version) {
// TODO this simply removes caret and tilde from version specification
// to convert something like "^3.6" to "3.6". Maybe a more suitable
// behaviour would be to convert it to 3.6, 3.7, 3.8 and 3.9
// FIXME this simply removes caret and tilde from version specification
// to convert something like "^3.6" to "3.6". The correct behavior
// would be to convert it to a range with 3.6, 3.7, 3.8 and 3.9
return version.replace(/[\^~]/, "");
}
}

// Didn't find what python version the project uses. Ask the user.
const version = await context.cli.askOption(QUESTION, PYTHON_VERSIONS);
// Save it in the configuration to avoid asking again
const pythonConfig = (context.config.plugins || {}).python || {};
pythonConfig.version = version;
context.config.plugins.python = pythonConfig;
await context.config.save();
return version;
context.errors.add({
title: ERR_UNDETECTABLE_TITLE,
message: ERR_UNDETECTABLE_INSTRUCTIONS,
});
};
2 changes: 2 additions & 0 deletions src/cli/commands/default.ts
Expand Up @@ -3,6 +3,7 @@ import { introspect } from "../../stack/mod.ts";
import { renderTemplates } from "../../template/mod.ts";
import { prelude } from "../prelude/mod.ts";
import { GlobalOptions } from "../types.ts";
import { outputErrors } from "../../plugin/errors.ts";

type DefaultOptions = GlobalOptions;

Expand All @@ -13,4 +14,5 @@ export default async function (opts: DefaultOptions): Promise<void> {
await platformWriters[platform](
renderTemplates(platform, detected),
);
outputErrors();
}
4 changes: 2 additions & 2 deletions src/cli/prelude/logger.ts
Expand Up @@ -32,8 +32,8 @@ function verboseHandler() {
function defaultHandler() {
const info = colors.bold.blue;
const warning = colors.bold.yellow;
const error = colors.bold.red;
const critical = colors.bold.bgRed;
const error = colors.bold;
const critical = colors.bold.red;
return new RawConsoleLogHandler("INFO", {
formatter: (logRecord) => {
const { levelName, msg } = logRecord;
Expand Down
29 changes: 29 additions & 0 deletions src/plugin/errors.ts
@@ -0,0 +1,29 @@
import { log } from "../../deps.ts";

interface IntrospectionError {
title: string;
message: string;
}

/**
* Holds a list of fatal errors for stack introspection
*/
const errorList: IntrospectionError[] = [];

export const errors = {
list: errorList,
add: (error: IntrospectionError) => {
errors.list.push(error);
},
};

export const outputErrors = () => {
if (!errors.list) return;
const logger = log.getLogger("main");
logger.warning(`Didn't generate pipeline for every detected stack!
`);
for (const error of errors.list) {
logger.critical(error.title);
logger.error(error.message);
}
};
4 changes: 4 additions & 0 deletions src/plugin/mod.ts
@@ -1,6 +1,7 @@
import { config } from "../config/mod.ts";
import { each, includes, readJSON, readLines, readToml } from "./files.ts";
import { askOption } from "./cli.ts";
import { errors } from "./errors.ts";
import { log, semver } from "../../deps.ts";

export const context = {
Expand All @@ -12,6 +13,9 @@ export const context = {
readToml,
readJSON,
},
errors: {
add: errors.add,
},
cli: {
askOption,
},
Expand Down
18 changes: 14 additions & 4 deletions src/stack/introspection.ts
Expand Up @@ -18,6 +18,13 @@ async function detected() {
return introspectors.filter((_, i) => detected[i]);
}

type Maybe<T> = T | undefined;

// Type predicate to filter undefined values
function isDefined<T>(v: Maybe<T>): v is T {
return v !== undefined;
}

/**
* Introspect each stack detected in the project and returns an object
* with the stack as a key and the introspected data as the value
Expand All @@ -35,15 +42,18 @@ export async function introspect() {
const stackNames = stack.map((t) => t.name).sort().join(", ");
logger.info(`Detected stack: ${stackNames}`);

const introspected = await Promise.all(
stack.map<Promise<ProjectData>>((introspector) =>
const introspected = (await Promise.all(
stack.map<Promise<Maybe<ProjectData>>>((introspector) =>
introspector.introspect(context)
),
);
));

const data = stack
.reduce((obj, introspector, i) => {
obj[introspector.name] = introspected[i];
const data = introspected[i];
if (isDefined(data)) {
obj[introspector.name] = data;
}
return obj;
}, {} as Stack);

Expand Down

0 comments on commit edd8197

Please sign in to comment.